From f8f2b0dd311760b32447b4453d2a83f122d4a66d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 8 Oct 2024 17:29:45 +0200 Subject: [PATCH 01/61] [Discover] Initial implementation --- .../src/components/app_menu/types.ts | 71 ++++++ packages/kbn-discover-utils/src/types.ts | 2 + .../components/top_nav/get_top_nav_links.tsx | 235 +++++++++--------- .../top_nav/run_app_menu_action.tsx | 188 ++++++++++++++ .../components/top_nav/run_share_action.ts | 114 +++++++++ .../top_nav/show_open_search_panel.tsx | 47 ---- 6 files changed, 488 insertions(+), 169 deletions(-) create mode 100644 packages/kbn-discover-utils/src/components/app_menu/types.ts create mode 100644 src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx create mode 100644 src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts delete mode 100644 src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts new file mode 100644 index 0000000000000..752e8b8022052 --- /dev/null +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -0,0 +1,71 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; + +export interface AppMenuControlOnClickParams { + anchorElement: HTMLElement; + onFinishAction: () => void; + // some discover specific props? +} + +export type AppMenuControlProps = Pick< + TopNavMenuData, + 'testId' | 'isLoading' | 'label' | 'description' | 'disableButton' | 'href' | 'tooltip' +> & { + onClick: ( + params: AppMenuControlOnClickParams + ) => Promise | React.ReactNode | void; +}; + +export type AppMenuIconControlProps = AppMenuControlProps & Pick; + +export enum AppMenuActionId { + new = 'new', + open = 'open', + share = 'share', + alerts = 'alerts', + inspect = 'inspect', +} + +export enum AppMenuActionType { + primary = 'primary', + secondary = 'secondary', + custom = 'custom', +} + +interface AppMenuActionBase { + id: AppMenuActionId | string; + order?: number; +} + +export interface AppMenuPopoverAction extends AppMenuActionBase { + type: AppMenuActionType.secondary | AppMenuActionType.custom; + controlProps: AppMenuControlProps; +} + +export interface AppMenuAction extends AppMenuActionBase { + type: AppMenuActionType.secondary | AppMenuActionType.custom; + controlProps: AppMenuControlProps; +} + +export interface AppMenuIconAction extends AppMenuActionBase { + type: AppMenuActionType.primary; + controlProps: AppMenuIconControlProps; +} + +export interface AppMenuPopoverActions extends AppMenuActionBase { + label: string; + type: AppMenuActionType.secondary | AppMenuActionType.custom; + actions: AppMenuPopoverAction[]; +} + +export type AppMenuItem = AppMenuPopoverActions | AppMenuAction | AppMenuIconAction; +export type AppMenuItems = AppMenuItem[]; diff --git a/packages/kbn-discover-utils/src/types.ts b/packages/kbn-discover-utils/src/types.ts index 63297edfe7643..2c298da999490 100644 --- a/packages/kbn-discover-utils/src/types.ts +++ b/packages/kbn-discover-utils/src/types.ts @@ -17,6 +17,8 @@ export type { RowControlProps, RowControlRowProps, } from './components/custom_control_columns/types'; +export type * from './components/app_menu/types'; +export { AppMenuActionId, AppMenuActionType } from './components/app_menu/types'; type DiscoverSearchHit = SearchHit>; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index a3115502e5ef3..ebd61eacb4c41 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -7,22 +7,27 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import React from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { omit } from 'lodash'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import type { DiscoverAppLocatorParams } from '../../../../../common'; +import { + AppMenuAction, + AppMenuActionId, + AppMenuActionType, + AppMenuItem, +} from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; -import { showOpenSearchPanel } from './show_open_search_panel'; -import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; import { DiscoverStateContainer } from '../../state_management/discover_state'; import { openAlertsPopover } from './open_alerts_popover'; import type { TopNavCustomization } from '../../../../customizations'; +import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; +import { runShareAction } from './run_share_action'; +import { OpenSearchPanel } from './open_search_panel'; /** * Helper function to build the top nav links @@ -114,17 +119,24 @@ export const getTopNavLinks = ({ testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', }; - const newSearch = { - id: 'new', - label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { - defaultMessage: 'New', - }), - description: i18n.translate('discover.localMenu.newSearchDescription', { - defaultMessage: 'New Search', - }), - run: () => services.locator.navigate({}), - testId: 'discoverNewButton', + const stateParams = { services, stateContainer: state, adHocDataViews, isEsqlMode }; + const newSearchItem: AppMenuAction = { + id: AppMenuActionId.new, + type: AppMenuActionType.secondary, // TODO: convert to primary + controlProps: { + label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { + defaultMessage: 'New', + }), + description: i18n.translate('discover.localMenu.newSearchDescription', { + defaultMessage: 'New Search', + }), + testId: 'discoverNewButton', + onClick: () => { + services.locator.navigate({}); + }, + }, }; + const newSearch = convertMenuItem({ appMenuItem: newSearchItem, stateParams }); const saveSearch = { id: 'save', @@ -149,21 +161,28 @@ export const getTopNavLinks = ({ }, }; - const openSearch = { - id: 'open', - label: i18n.translate('discover.localMenu.openTitle', { - defaultMessage: 'Open', - }), - description: i18n.translate('discover.localMenu.openSavedSearchDescription', { - defaultMessage: 'Open Saved Search', - }), - testId: 'discoverOpenButton', - run: () => - showOpenSearchPanel({ - onOpenSavedSearch: state.actions.onOpenSavedSearch, - services, + const openSearchItem: AppMenuAction = { + id: AppMenuActionId.open, + type: AppMenuActionType.secondary, // TODO: convert to primary + controlProps: { + label: i18n.translate('discover.localMenu.openTitle', { + defaultMessage: 'Open', + }), + description: i18n.translate('discover.localMenu.openSavedSearchDescription', { + defaultMessage: 'Open Saved Search', }), + testId: 'discoverOpenButton', + onClick: ({ onFinishAction }) => { + return ( + + ); + }, + }, }; + const openSearch = convertMenuItem({ appMenuItem: openSearchItem, stateParams }); const shareSearch = { id: 'share', @@ -175,107 +194,33 @@ export const getTopNavLinks = ({ }), testId: 'shareTopNavButton', run: async (anchorElement: HTMLElement) => { - if (!services.share) return; - const savedSearch = state.savedSearchState.getState(); - const searchSourceSharingData = await getSharingData( - savedSearch.searchSource, - state.appState.getState(), - services, - isEsqlMode - ); - - const { locator, notifications } = services; - const appState = state.appState.getState(); - const { timefilter } = services.data.query.timefilter; - const timeRange = timefilter.getTime(); - const refreshInterval = timefilter.getRefreshInterval(); - const filters = services.filterManager.getFilters(); - - // Share -> Get links -> Snapshot - const params: DiscoverAppLocatorParams = { - ...omit(appState, 'dataSource'), - ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), - ...(dataView?.isPersisted() - ? { dataViewId: dataView?.id } - : { dataViewSpec: dataView?.toMinimalSpec() }), - filters, - timeRange, - refreshInterval, - }; - const relativeUrl = locator.getRedirectUrl(params); - - // This logic is duplicated from `relativeToAbsolute` (for bundle size reasons). Ultimately, this should be - // replaced when https://github.com/elastic/kibana/issues/153323 is implemented. - const link = document.createElement('a'); - link.setAttribute('href', relativeUrl); - const shareableUrl = link.href; - - // Share -> Get links -> Saved object - let shareableUrlForSavedObject = await locator.getUrl( - { savedSearchId: savedSearch.id }, - { absolute: true } - ); - - // UrlPanelContent forces a '_g' parameter in the saved object URL: - // https://github.com/elastic/kibana/blob/a30508153c1467b1968fb94faf1debc5407f61ea/src/plugins/share/public/components/url_panel_content.tsx#L230 - // Since our locator doesn't add the '_g' parameter if it's not needed, UrlPanelContent - // will interpret it as undefined and add '?_g=' to the URL, which is invalid in Discover, - // so instead we add an empty object for the '_g' parameter to the URL. - shareableUrlForSavedObject = setStateToKbnUrl( - '_g', - {}, - undefined, - shareableUrlForSavedObject - ); - - services.share.toggleShareContextMenu({ + await runShareAction({ anchorElement, - allowEmbed: false, - allowShortUrl: !!services.capabilities.discover.createShortUrl, - shareableUrl, - shareableUrlForSavedObject, - shareableUrlLocatorParams: { locator, params }, - objectId: savedSearch.id, - objectType: 'search', - objectTypeMeta: { - title: i18n.translate('discover.share.shareModal.title', { - defaultMessage: 'Share this search', - }), - }, - sharingData: { - isTextBased: isEsqlMode, - locatorParams: [{ id: locator.id, params }], - ...searchSourceSharingData, - // CSV reports can be generated without a saved search so we provide a fallback title - title: - savedSearch.title || - i18n.translate('discover.localMenu.fallbackReportTitle', { - defaultMessage: 'Untitled discover search', - }), - }, - isDirty: !savedSearch.id || state.appState.hasChanged(), - showPublicUrlSwitch, - onClose: () => { - anchorElement?.focus(); - }, - toasts: notifications.toasts, + dataView, + stateContainer: state, + services, + isEsqlMode, }); }, }; - const inspectSearch = { - id: 'inspect', - label: i18n.translate('discover.localMenu.inspectTitle', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', { - defaultMessage: 'Open Inspector for search', - }), - testId: 'openInspectorButton', - run: () => { - onOpenInspector(); + const inspectSearchItem: AppMenuAction = { + id: AppMenuActionId.inspect, + type: AppMenuActionType.secondary, + controlProps: { + label: i18n.translate('discover.localMenu.inspectTitle', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', { + defaultMessage: 'Open Inspector for search', + }), + testId: 'openInspectorButton', + onClick: () => { + onOpenInspector(); + }, }, }; + const inspectSearch = convertMenuItem({ appMenuItem: inspectSearchItem, stateParams }); const defaultMenu = topNavCustomization?.defaultMenu; const entries = [...(topNavCustomization?.getMenuItems?.() ?? [])]; @@ -314,3 +259,49 @@ export const getTopNavLinks = ({ return entries.sort((a, b) => a.order - b.order).map((entry) => entry.data); }; + +function convertMenuItem({ + appMenuItem, + stateParams: { services, stateContainer, adHocDataViews, isEsqlMode }, +}: { + appMenuItem: AppMenuItem; + stateParams: { + stateContainer: DiscoverStateContainer; + services: DiscoverServices; + adHocDataViews: DataView[]; + isEsqlMode?: boolean; + }; +}): TopNavMenuData { + if ('actions' in appMenuItem) { + return { + id: appMenuItem.id, + label: appMenuItem.label, + description: appMenuItem.label, + run: (anchorElement: HTMLElement) => { + runAppMenuPopoverAction({ + appMenuItem, + anchorElement, + stateContainer, + adHocDataViews, + services, + isEsqlMode, + }); + }, + testId: appMenuItem.id, + }; + } + + return { + id: appMenuItem.id, + label: appMenuItem.controlProps.label, + description: appMenuItem.controlProps.label, + run: async (anchorElement: HTMLElement) => { + await runAppMenuAction({ + appMenuItem, + anchorElement, + services, + }); + }, + testId: appMenuItem.id, + }; +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx new file mode 100644 index 0000000000000..bae9a49d9fb3c --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx @@ -0,0 +1,188 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; +import type { DataView } from '@kbn/data-plugin/common'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { AppMenuAction, AppMenuIconAction, AppMenuPopoverActions } from '@kbn/discover-utils'; +import { DiscoverStateContainer } from '../../state_management/discover_state'; +import { DiscoverServices } from '../../../../build_services'; + +const container = document.createElement('div'); +let isOpen = false; + +interface AppMenuActionsMenuPopoverProps { + appMenuItem: AppMenuPopoverActions; + anchorElement: HTMLElement; + stateContainer: DiscoverStateContainer; + adHocDataViews: DataView[]; + services: DiscoverServices; + isEsqlMode?: boolean; + onClose: () => void; +} + +export const AppMenuActionsMenuPopover: React.FC = ({ + appMenuItem, + anchorElement, + onClose: originalOnClose, +}) => { + const [nestedContent, setNestedContent] = useState(); + + const onClose = useCallback(() => { + originalOnClose(); + anchorElement?.focus(); + }, [anchorElement, originalOnClose]); + + const panels = [ + { + id: appMenuItem.id, + name: appMenuItem.label, + items: appMenuItem.actions.map((action) => { + const controlProps = action.controlProps; + + return { + name: controlProps.label, + disabled: + typeof controlProps.disableButton === 'function' + ? controlProps.disableButton() + : Boolean(controlProps.disableButton), + onClick: async () => { + const result = await controlProps.onClick({ + anchorElement, + onFinishAction: onClose, + }); + + if (result) { + setNestedContent(result); + } + }, + href: controlProps.href, + ['data-test-subj']: controlProps.testId, + toolTipContent: + typeof controlProps.tooltip === 'function' + ? controlProps.tooltip() + : controlProps.tooltip, + }; + }), + }, + ]; + + return ( + <> + {nestedContent} + + + + + ); +}; + +function cleanup() { + if (!isOpen) { + return; + } + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; +} + +export function runAppMenuPopoverAction({ + appMenuItem, + anchorElement, + stateContainer, + services, + adHocDataViews, + isEsqlMode, +}: { + appMenuItem: AppMenuPopoverActions; + anchorElement: HTMLElement; + stateContainer: DiscoverStateContainer; + services: DiscoverServices; + adHocDataViews: DataView[]; + isEsqlMode?: boolean; +}) { + if (isOpen) { + cleanup(); + return; + } + + isOpen = true; + document.body.appendChild(container); + + const element = ( + + + + + + ); + ReactDOM.render(element, container); +} + +export async function runAppMenuAction({ + appMenuItem, + anchorElement, + services, +}: { + appMenuItem: AppMenuAction | AppMenuIconAction; + anchorElement: HTMLElement; + services: DiscoverServices; +}) { + cleanup(); + + const controlProps = appMenuItem.controlProps; + + const result = await controlProps.onClick({ + anchorElement, + onFinishAction: () => { + cleanup(); + anchorElement?.focus(); + }, + }); + + if (!result) { + return; + } + + isOpen = true; + document.body.appendChild(container); + + const element = ( + + {result} + + ); + ReactDOM.render(element, container); +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts b/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts new file mode 100644 index 0000000000000..17a08d43cdddd --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts @@ -0,0 +1,114 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { omit } from 'lodash'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { DiscoverStateContainer } from '../../state_management/discover_state'; +import { DiscoverServices } from '../../../../build_services'; +import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data'; +import { DiscoverAppLocatorParams } from '../../../../../common/app_locator'; + +export async function runShareAction({ + anchorElement, + dataView, + stateContainer, + services, + isEsqlMode, +}: { + anchorElement: HTMLElement; + dataView: DataView | undefined; + stateContainer: DiscoverStateContainer; + services: DiscoverServices; + isEsqlMode?: boolean; +}) { + if (!services.share) { + return; + } + const savedSearch = stateContainer.savedSearchState.getState(); + const searchSourceSharingData = await getSharingData( + savedSearch.searchSource, + stateContainer.appState.getState(), + services, + isEsqlMode + ); + + const { locator, notifications } = services; + const appState = stateContainer.appState.getState(); + const { timefilter } = services.data.query.timefilter; + const timeRange = timefilter.getTime(); + const refreshInterval = timefilter.getRefreshInterval(); + const filters = services.filterManager.getFilters(); + + // Share -> Get links -> Snapshot + const params: DiscoverAppLocatorParams = { + ...omit(appState, 'dataSource'), + ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), + ...(dataView?.isPersisted() + ? { dataViewId: dataView?.id } + : { dataViewSpec: dataView?.toMinimalSpec() }), + filters, + timeRange, + refreshInterval, + }; + const relativeUrl = locator.getRedirectUrl(params); + + // This logic is duplicated from `relativeToAbsolute` (for bundle size reasons). Ultimately, this should be + // replaced when https://github.com/elastic/kibana/issues/153323 is implemented. + const link = document.createElement('a'); + link.setAttribute('href', relativeUrl); + const shareableUrl = link.href; + + // Share -> Get links -> Saved object + let shareableUrlForSavedObject = await locator.getUrl( + { savedSearchId: savedSearch.id }, + { absolute: true } + ); + + // UrlPanelContent forces a '_g' parameter in the saved object URL: + // https://github.com/elastic/kibana/blob/a30508153c1467b1968fb94faf1debc5407f61ea/src/plugins/share/public/components/url_panel_content.tsx#L230 + // Since our locator doesn't add the '_g' parameter if it's not needed, UrlPanelContent + // will interpret it as undefined and add '?_g=' to the URL, which is invalid in Discover, + // so instead we add an empty object for the '_g' parameter to the URL. + shareableUrlForSavedObject = setStateToKbnUrl('_g', {}, undefined, shareableUrlForSavedObject); + + services.share.toggleShareContextMenu({ + anchorElement, + allowEmbed: false, + allowShortUrl: !!services.capabilities.discover.createShortUrl, + shareableUrl, + shareableUrlForSavedObject, + shareableUrlLocatorParams: { locator, params }, + objectId: savedSearch.id, + objectType: 'search', + objectTypeMeta: { + title: i18n.translate('discover.share.shareModal.title', { + defaultMessage: 'Share this search', + }), + }, + sharingData: { + isTextBased: isEsqlMode, + locatorParams: [{ id: locator.id, params }], + ...searchSourceSharingData, + // CSV reports can be generated without a saved search so we provide a fallback title + title: + savedSearch.title || + i18n.translate('discover.localMenu.fallbackReportTitle', { + defaultMessage: 'Untitled discover search', + }), + }, + isDirty: !savedSearch.id || stateContainer.appState.hasChanged(), + showPublicUrlSwitch, + onClose: () => { + anchorElement?.focus(); + }, + toasts: notifications.toasts, + }); +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx deleted file mode 100644 index 9413a46e998d8..0000000000000 --- a/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { DiscoverServices } from '../../../../build_services'; -import { OpenSearchPanel } from './open_search_panel'; - -let isOpen = false; - -export function showOpenSearchPanel({ - onOpenSavedSearch, - services, -}: { - onOpenSavedSearch: (id: string) => void; - services: DiscoverServices; -}) { - if (isOpen) { - return; - } - - isOpen = true; - const container = document.createElement('div'); - const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpen = false; - }; - - document.body.appendChild(container); - const element = ( - - - - - - ); - ReactDOM.render(element, container); -} From 6cf8d907131f25002a20bb622ef56bd79611d7ca Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:44:51 +0000 Subject: [PATCH 02/61] [CI] Auto-commit changed files from 'node scripts/notice' --- packages/kbn-discover-utils/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index 724051e5863c4..01e0a315d0935 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -27,6 +27,7 @@ "@kbn/core-ui-settings-browser", "@kbn/ui-theme", "@kbn/expressions-plugin", - "@kbn/logs-data-access-plugin" + "@kbn/logs-data-access-plugin", + "@kbn/navigation-plugin" ] } From 2e96f8d82afab29a4c03aca9e048fbc58b6a4918 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 8 Oct 2024 20:57:12 +0200 Subject: [PATCH 03/61] [Discover] Refactor further --- .../src/components/app_menu/types.ts | 35 ++- .../components/top_nav/get_action_alerts.tsx | 162 ++++++++++++ .../components/top_nav/get_top_nav_links.tsx | 63 ++--- .../top_nav/open_alerts_popover.tsx | 237 ------------------ .../top_nav/run_app_menu_action.tsx | 50 ++-- 5 files changed, 243 insertions(+), 304 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx delete mode 100644 src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 752e8b8022052..1fd8c80027dc6 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -9,20 +9,43 @@ import React from 'react'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Query } from '@kbn/es-query'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; + +export interface AppMenuDiscoverParams { + dataView: DataView | undefined; + adHocDataViews: DataView[]; + isEsqlMode?: boolean; + query?: Query | AggregateQuery; + savedQueryId?: string; + savedSearch: SavedSearch; + services: { + core: CoreStart; + application: ApplicationStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + }; + onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; +} export interface AppMenuControlOnClickParams { anchorElement: HTMLElement; + getDiscoverParams: () => AppMenuDiscoverParams; onFinishAction: () => void; - // some discover specific props? } export type AppMenuControlProps = Pick< TopNavMenuData, 'testId' | 'isLoading' | 'label' | 'description' | 'disableButton' | 'href' | 'tooltip' > & { - onClick: ( - params: AppMenuControlOnClickParams - ) => Promise | React.ReactNode | void; + onClick: + | (( + params: AppMenuControlOnClickParams + ) => Promise | React.ReactNode | void) + | undefined; }; export type AppMenuIconControlProps = AppMenuControlProps & Pick; @@ -62,7 +85,9 @@ export interface AppMenuIconAction extends AppMenuActionBase { } export interface AppMenuPopoverActions extends AppMenuActionBase { - label: string; + label: TopNavMenuData['label']; + description?: TopNavMenuData['description']; + testId?: TopNavMenuData['testId']; type: AppMenuActionType.secondary | AppMenuActionType.custom; actions: AppMenuPopoverAction[]; } diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx new file mode 100644 index 0000000000000..5fd6311701264 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx @@ -0,0 +1,162 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useMemo } from 'react'; +import type { DataView } from '@kbn/data-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { + AppMenuActionId, + AppMenuActionType, + AppMenuDiscoverParams, + AppMenuPopoverActions, +} from '@kbn/discover-utils'; +import { + AlertConsumers, + ES_QUERY_ID, + RuleCreationValidConsumer, + STACK_ALERTS_FEATURE_ID, +} from '@kbn/rule-data-utils'; +import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; + +const EsQueryValidConsumer: RuleCreationValidConsumer[] = [ + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.OBSERVABILITY, + STACK_ALERTS_FEATURE_ID, +]; + +interface EsQueryAlertMetaData extends RuleTypeMetaData { + isManagementPage?: boolean; + adHocDataViewList: DataView[]; +} + +const CreateAlertFlyout: React.FC<{ + getDiscoverParams: () => AppMenuDiscoverParams; + onFinishAction: () => void; +}> = React.memo(({ getDiscoverParams, onFinishAction }) => { + const { + dataView, + services, + savedQueryId, + savedSearch, + isEsqlMode, + query, + adHocDataViews, + onUpdateAdHocDataViews, + } = getDiscoverParams(); + const { triggersActionsUi } = services; + const timeField = getTimeField(dataView); + + /** + * Provides the default parameters used to initialize the new rule + */ + const getParams = useCallback(() => { + if (isEsqlMode) { + return { + searchType: 'esqlQuery', + esqlQuery: query, + timeField, + }; + } + return { + searchType: 'searchSource', + searchConfiguration: savedSearch.searchSource.getSerializedFields(), + savedQueryId, + }; + }, [isEsqlMode, savedSearch, savedQueryId, query, timeField]); + + const discoverMetadata: EsQueryAlertMetaData = useMemo( + () => ({ + isManagementPage: false, + adHocDataViewList: adHocDataViews, + }), + [adHocDataViews] + ); + + return triggersActionsUi?.getAddRuleFlyout({ + metadata: discoverMetadata, + consumer: 'alerts', + onClose: (_, metadata) => { + onUpdateAdHocDataViews(metadata!.adHocDataViewList); + onFinishAction(); + }, + onSave: async (metadata) => { + onUpdateAdHocDataViews(metadata!.adHocDataViewList); + }, + canChangeTrigger: false, + ruleTypeId: ES_QUERY_ID, + initialValues: { params: getParams() }, + validConsumers: EsQueryValidConsumer, + useRuleProducer: true, + // Default to the Logs consumer if it's available. This should fall back to Stack Alerts if it's not. + initialSelectedConsumer: AlertConsumers.LOGS, + }); +}); + +export const getAlertsActionItem = ({ + getDiscoverParams, +}: { + getDiscoverParams: () => AppMenuDiscoverParams; +}): AppMenuPopoverActions => { + const { dataView, services, isEsqlMode } = getDiscoverParams(); + const timeField = getTimeField(dataView); + const hasTimeFieldName = !isEsqlMode ? Boolean(dataView?.timeFieldName) : Boolean(timeField); + + return { + id: AppMenuActionId.alerts, + type: AppMenuActionType.secondary, + label: i18n.translate('discover.localMenu.localMenu.alertsTitle', { + defaultMessage: 'Alerts', + }), + description: i18n.translate('discover.localMenu.alertsDescription', { + defaultMessage: 'Alerts', + }), + testId: 'discoverAlertsButton', + actions: [ + { + id: 'createSearchThreshold', + type: AppMenuActionType.secondary, + controlProps: { + label: i18n.translate('discover.alerts.createSearchThreshold', { + defaultMessage: 'Create search threshold rule', + }), + testId: 'discoverCreateAlertButton', + disableButton: !hasTimeFieldName, + tooltip: hasTimeFieldName + ? undefined + : i18n.translate('discover.alerts.missedTimeFieldToolTip', { + defaultMessage: 'Data view does not have a time field.', + }), + onClick: async (params) => { + return ; + }, + }, + }, + { + id: 'manageRulesAndConnectors', + type: AppMenuActionType.secondary, + controlProps: { + label: i18n.translate('discover.alerts.manageRulesAndConnectors', { + defaultMessage: 'Manage rules and connectors', + }), + testId: 'discoverManageAlertsButton', + href: services.application.getUrlForApp( + 'management/insightsAndAlerting/triggersActions/rules' + ), + onClick: undefined, + }, + }, + ], + }; +}; + +function getTimeField(dataView: DataView | undefined) { + const dateFields = dataView?.fields.getByType('date'); + return dataView?.timeFieldName || dateFields?.[0]?.name; +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index ebd61eacb4c41..610096a5afc51 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -17,13 +17,14 @@ import { AppMenuAction, AppMenuActionId, AppMenuActionType, + AppMenuDiscoverParams, AppMenuItem, } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { openAlertsPopover } from './open_alerts_popover'; +import { getAlertsActionItem } from './get_action_alerts'; import type { TopNavCustomization } from '../../../../customizations'; import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; import { runShareAction } from './run_share_action'; @@ -51,25 +52,22 @@ export const getTopNavLinks = ({ topNavCustomization: TopNavCustomization | undefined; shouldShowESQLToDataViewTransitionModal: boolean; }): TopNavMenuData[] => { - const alerts = { - id: 'alerts', - label: i18n.translate('discover.localMenu.localMenu.alertsTitle', { - defaultMessage: 'Alerts', - }), - description: i18n.translate('discover.localMenu.alertsDescription', { - defaultMessage: 'Alerts', - }), - run: async (anchorElement: HTMLElement) => { - openAlertsPopover({ - anchorElement, - services, - stateContainer: state, - adHocDataViews, - isEsqlMode, - }); + const getDiscoverParams = (): AppMenuDiscoverParams => ({ + dataView, + adHocDataViews, + isEsqlMode, + services, + savedSearch: state.savedSearchState.getState(), + savedQueryId: state.appState.getState().savedQuery, + query: state.appState.getState().query, + onUpdateAdHocDataViews: async (adHocDataViewList) => { + await state.actions.loadDataViewList(); + state.internalState.transitions.setAdHocDataViews(adHocDataViewList); }, - testId: 'discoverAlertsButton', - }; + }); + + const alertsItem = getAlertsActionItem({ getDiscoverParams }); + const alerts = convertMenuItem({ appMenuItem: alertsItem, getDiscoverParams }); /** * Switches from ES|QL to classic mode and vice versa @@ -119,7 +117,6 @@ export const getTopNavLinks = ({ testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', }; - const stateParams = { services, stateContainer: state, adHocDataViews, isEsqlMode }; const newSearchItem: AppMenuAction = { id: AppMenuActionId.new, type: AppMenuActionType.secondary, // TODO: convert to primary @@ -136,7 +133,7 @@ export const getTopNavLinks = ({ }, }, }; - const newSearch = convertMenuItem({ appMenuItem: newSearchItem, stateParams }); + const newSearch = convertMenuItem({ appMenuItem: newSearchItem, getDiscoverParams }); const saveSearch = { id: 'save', @@ -182,7 +179,7 @@ export const getTopNavLinks = ({ }, }, }; - const openSearch = convertMenuItem({ appMenuItem: openSearchItem, stateParams }); + const openSearch = convertMenuItem({ appMenuItem: openSearchItem, getDiscoverParams }); const shareSearch = { id: 'share', @@ -220,7 +217,7 @@ export const getTopNavLinks = ({ }, }, }; - const inspectSearch = convertMenuItem({ appMenuItem: inspectSearchItem, stateParams }); + const inspectSearch = convertMenuItem({ appMenuItem: inspectSearchItem, getDiscoverParams }); const defaultMenu = topNavCustomization?.defaultMenu; const entries = [...(topNavCustomization?.getMenuItems?.() ?? [])]; @@ -262,29 +259,21 @@ export const getTopNavLinks = ({ function convertMenuItem({ appMenuItem, - stateParams: { services, stateContainer, adHocDataViews, isEsqlMode }, + getDiscoverParams, }: { appMenuItem: AppMenuItem; - stateParams: { - stateContainer: DiscoverStateContainer; - services: DiscoverServices; - adHocDataViews: DataView[]; - isEsqlMode?: boolean; - }; + getDiscoverParams: () => AppMenuDiscoverParams; }): TopNavMenuData { if ('actions' in appMenuItem) { return { id: appMenuItem.id, label: appMenuItem.label, - description: appMenuItem.label, + description: appMenuItem.description ?? appMenuItem.label, run: (anchorElement: HTMLElement) => { runAppMenuPopoverAction({ appMenuItem, anchorElement, - stateContainer, - adHocDataViews, - services, - isEsqlMode, + getDiscoverParams, }); }, testId: appMenuItem.id, @@ -294,12 +283,12 @@ function convertMenuItem({ return { id: appMenuItem.id, label: appMenuItem.controlProps.label, - description: appMenuItem.controlProps.label, + description: appMenuItem.controlProps.description ?? appMenuItem.controlProps.label, run: async (anchorElement: HTMLElement) => { await runAppMenuAction({ appMenuItem, anchorElement, - services, + getDiscoverParams, }); }, testId: appMenuItem.id, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx deleted file mode 100644 index e16f71406bb90..0000000000000 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ /dev/null @@ -1,237 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useCallback, useState, useMemo } from 'react'; -import ReactDOM from 'react-dom'; -import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { DataView } from '@kbn/data-plugin/common'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { - AlertConsumers, - ES_QUERY_ID, - RuleCreationValidConsumer, - STACK_ALERTS_FEATURE_ID, -} from '@kbn/rule-data-utils'; -import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; -import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { DiscoverServices } from '../../../../build_services'; - -const container = document.createElement('div'); -let isOpen = false; - -const EsQueryValidConsumer: RuleCreationValidConsumer[] = [ - AlertConsumers.INFRASTRUCTURE, - AlertConsumers.LOGS, - AlertConsumers.OBSERVABILITY, - STACK_ALERTS_FEATURE_ID, -]; - -interface AlertsPopoverProps { - onClose: () => void; - anchorElement: HTMLElement; - stateContainer: DiscoverStateContainer; - savedQueryId?: string; - adHocDataViews: DataView[]; - services: DiscoverServices; - isEsqlMode?: boolean; -} - -interface EsQueryAlertMetaData extends RuleTypeMetaData { - isManagementPage?: boolean; - adHocDataViewList: DataView[]; -} - -export function AlertsPopover({ - anchorElement, - adHocDataViews, - services, - stateContainer, - onClose: originalOnClose, - isEsqlMode, -}: AlertsPopoverProps) { - const dataView = stateContainer.internalState.getState().dataView; - const query = stateContainer.appState.getState().query; - const dateFields = dataView?.fields.getByType('date'); - const timeField = dataView?.timeFieldName || dateFields?.[0]?.name; - - const { triggersActionsUi } = services; - const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); - const onClose = useCallback(() => { - originalOnClose(); - anchorElement?.focus(); - }, [anchorElement, originalOnClose]); - - /** - * Provides the default parameters used to initialize the new rule - */ - const getParams = useCallback(() => { - if (isEsqlMode) { - return { - searchType: 'esqlQuery', - esqlQuery: query, - timeField, - }; - } - const savedQueryId = stateContainer.appState.getState().savedQuery; - return { - searchType: 'searchSource', - searchConfiguration: stateContainer.savedSearchState - .getState() - .searchSource.getSerializedFields(), - savedQueryId, - }; - }, [isEsqlMode, stateContainer.appState, stateContainer.savedSearchState, query, timeField]); - - const discoverMetadata: EsQueryAlertMetaData = useMemo( - () => ({ - isManagementPage: false, - adHocDataViewList: adHocDataViews, - }), - [adHocDataViews] - ); - - const SearchThresholdAlertFlyout = useMemo(() => { - if (!alertFlyoutVisible) { - return; - } - - const onFinishFlyoutInteraction = async (metadata: EsQueryAlertMetaData) => { - await stateContainer.actions.loadDataViewList(); - stateContainer.internalState.transitions.setAdHocDataViews(metadata.adHocDataViewList); - }; - - return triggersActionsUi?.getAddRuleFlyout({ - metadata: discoverMetadata, - consumer: 'alerts', - onClose: (_, metadata) => { - onFinishFlyoutInteraction(metadata!); - onClose(); - }, - onSave: async (metadata) => { - onFinishFlyoutInteraction(metadata!); - }, - canChangeTrigger: false, - ruleTypeId: ES_QUERY_ID, - initialValues: { params: getParams() }, - validConsumers: EsQueryValidConsumer, - useRuleProducer: true, - // Default to the Logs consumer if it's available. This should fall back to Stack Alerts if it's not. - initialSelectedConsumer: AlertConsumers.LOGS, - }); - }, [alertFlyoutVisible, triggersActionsUi, discoverMetadata, getParams, onClose, stateContainer]); - - const hasTimeFieldName: boolean = useMemo(() => { - if (!isEsqlMode) { - return Boolean(dataView?.timeFieldName); - } else { - return Boolean(timeField); - } - }, [dataView?.timeFieldName, isEsqlMode, timeField]); - - const panels = [ - { - id: 'mainPanel', - name: 'Alerting', - items: [ - { - name: ( - - ), - icon: 'bell', - onClick: () => setAlertFlyoutVisibility(true), - disabled: !hasTimeFieldName, - toolTipContent: hasTimeFieldName ? undefined : ( - - ), - ['data-test-subj']: 'discoverCreateAlertButton', - }, - { - name: ( - - ), - icon: 'tableOfContents', - href: services?.application?.getUrlForApp( - 'management/insightsAndAlerting/triggersActions/rules' - ), - ['data-test-subj']: 'discoverManageAlertsButton', - }, - ], - }, - ]; - - return ( - <> - {SearchThresholdAlertFlyout} - - - - - ); -} - -function closeAlertsPopover() { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpen = false; -} - -export function openAlertsPopover({ - anchorElement, - stateContainer, - services, - adHocDataViews, - isEsqlMode, -}: { - anchorElement: HTMLElement; - stateContainer: DiscoverStateContainer; - services: DiscoverServices; - adHocDataViews: DataView[]; - isEsqlMode?: boolean; -}) { - if (isOpen) { - closeAlertsPopover(); - return; - } - - isOpen = true; - document.body.appendChild(container); - - const element = ( - - - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx index bae9a49d9fb3c..f8c583afca100 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx @@ -19,12 +19,14 @@ import React, { useCallback, useState } from 'react'; import ReactDOM from 'react-dom'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; -import type { DataView } from '@kbn/data-plugin/common'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { AppMenuAction, AppMenuIconAction, AppMenuPopoverActions } from '@kbn/discover-utils'; -import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { DiscoverServices } from '../../../../build_services'; +import type { + AppMenuAction, + AppMenuDiscoverParams, + AppMenuIconAction, + AppMenuPopoverActions, +} from '@kbn/discover-utils'; const container = document.createElement('div'); let isOpen = false; @@ -32,16 +34,14 @@ let isOpen = false; interface AppMenuActionsMenuPopoverProps { appMenuItem: AppMenuPopoverActions; anchorElement: HTMLElement; - stateContainer: DiscoverStateContainer; - adHocDataViews: DataView[]; - services: DiscoverServices; - isEsqlMode?: boolean; + getDiscoverParams: () => AppMenuDiscoverParams; onClose: () => void; } export const AppMenuActionsMenuPopover: React.FC = ({ appMenuItem, anchorElement, + getDiscoverParams, onClose: originalOnClose, }) => { const [nestedContent, setNestedContent] = useState(); @@ -65,8 +65,9 @@ export const AppMenuActionsMenuPopover: React.FC ? controlProps.disableButton() : Boolean(controlProps.disableButton), onClick: async () => { - const result = await controlProps.onClick({ + const result = await controlProps.onClick?.({ anchorElement, + getDiscoverParams, onFinishAction: onClose, }); @@ -95,7 +96,12 @@ export const AppMenuActionsMenuPopover: React.FC isOpen={!nestedContent} panelPaddingSize="s" > - + ); @@ -113,17 +119,11 @@ function cleanup() { export function runAppMenuPopoverAction({ appMenuItem, anchorElement, - stateContainer, - services, - adHocDataViews, - isEsqlMode, + getDiscoverParams, }: { appMenuItem: AppMenuPopoverActions; anchorElement: HTMLElement; - stateContainer: DiscoverStateContainer; - services: DiscoverServices; - adHocDataViews: DataView[]; - isEsqlMode?: boolean; + getDiscoverParams: () => AppMenuDiscoverParams; }) { if (isOpen) { cleanup(); @@ -132,6 +132,7 @@ export function runAppMenuPopoverAction({ isOpen = true; document.body.appendChild(container); + const { services } = getDiscoverParams(); const element = ( @@ -139,10 +140,7 @@ export function runAppMenuPopoverAction({ @@ -154,18 +152,20 @@ export function runAppMenuPopoverAction({ export async function runAppMenuAction({ appMenuItem, anchorElement, - services, + getDiscoverParams, }: { appMenuItem: AppMenuAction | AppMenuIconAction; anchorElement: HTMLElement; - services: DiscoverServices; + getDiscoverParams: () => AppMenuDiscoverParams; }) { cleanup(); + const { services } = getDiscoverParams(); const controlProps = appMenuItem.controlProps; - const result = await controlProps.onClick({ + const result = await controlProps.onClick?.({ anchorElement, + getDiscoverParams, onFinishAction: () => { cleanup(); anchorElement?.focus(); From 1a2ba08af46e6e86f1128d961f1fd275c4cce5fe Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 8 Oct 2024 21:43:40 +0200 Subject: [PATCH 04/61] [Discover] Extract actions --- .../src/components/app_menu/types.ts | 7 + .../get_alerts.tsx} | 2 +- .../top_nav/app_menu_actions/get_inspect.tsx | 34 ++++ .../app_menu_actions/get_new_search.tsx | 31 +++ .../app_menu_actions/get_open_search.tsx | 33 ++++ .../top_nav/app_menu_actions/get_share.tsx | 131 +++++++++++++ .../top_nav/app_menu_actions/index.ts | 14 ++ .../components/top_nav/get_top_nav_links.tsx | 184 +++++++----------- .../components/top_nav/run_share_action.ts | 114 ----------- 9 files changed, 319 insertions(+), 231 deletions(-) rename src/plugins/discover/public/application/main/components/top_nav/{get_action_alerts.tsx => app_menu_actions/get_alerts.tsx} (99%) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts delete mode 100644 src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 1fd8c80027dc6..354b511db5b86 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -15,6 +15,8 @@ import type { ApplicationStart } from '@kbn/core-application-browser'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public'; +import type { Capabilities } from '@kbn/core-capabilities-common'; export interface AppMenuDiscoverParams { dataView: DataView | undefined; @@ -27,8 +29,13 @@ export interface AppMenuDiscoverParams { core: CoreStart; application: ApplicationStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + data: DataPublicPluginStart; + filterManager: FilterManager; + capabilities: Capabilities; }; onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; + onNewSearch: () => void; + onOpenSavedSearch: (id: string) => void; } export interface AppMenuControlOnClickParams { diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx similarity index 99% rename from src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx rename to src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index 5fd6311701264..d91ba7e485e38 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_action_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -99,7 +99,7 @@ const CreateAlertFlyout: React.FC<{ }); }); -export const getAlertsActionItem = ({ +export const getAlertsAppMenuItem = ({ getDiscoverParams, }: { getDiscoverParams: () => AppMenuDiscoverParams; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx new file mode 100644 index 0000000000000..ddd531fc81d3f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx @@ -0,0 +1,34 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; + +export const getInspectAppMenuItem = ({ + onOpenInspector, +}: { + onOpenInspector: () => void; +}): AppMenuAction => { + return { + id: AppMenuActionId.inspect, + type: AppMenuActionType.secondary, + controlProps: { + label: i18n.translate('discover.localMenu.inspectTitle', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', { + defaultMessage: 'Open Inspector for search', + }), + testId: 'openInspectorButton', + onClick: () => { + onOpenInspector(); + }, + }, + }; +}; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx new file mode 100644 index 0000000000000..3efe874c362d7 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -0,0 +1,31 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; + +export const getNewSearchAppMenuItem = (): AppMenuAction => { + return { + id: AppMenuActionId.new, + type: AppMenuActionType.secondary, // TODO: convert to primary + controlProps: { + label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { + defaultMessage: 'New', + }), + description: i18n.translate('discover.localMenu.newSearchDescription', { + defaultMessage: 'New Search', + }), + testId: 'discoverNewButton', + onClick: ({ getDiscoverParams }) => { + const { onNewSearch } = getDiscoverParams(); + onNewSearch(); + }, + }, + }; +}; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx new file mode 100644 index 0000000000000..d1c0faf059f38 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -0,0 +1,33 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; +import { OpenSearchPanel } from '../open_search_panel'; + +export const getOpenSearchAppMenuItem = (): AppMenuAction => { + return { + id: AppMenuActionId.open, + type: AppMenuActionType.secondary, // TODO: convert to primary + controlProps: { + label: i18n.translate('discover.localMenu.openTitle', { + defaultMessage: 'Open', + }), + description: i18n.translate('discover.localMenu.openSavedSearchDescription', { + defaultMessage: 'Open Saved Search', + }), + testId: 'discoverOpenButton', + onClick: ({ getDiscoverParams, onFinishAction }) => { + const { onOpenSavedSearch } = getDiscoverParams(); + return ; + }, + }, + }; +}; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx new file mode 100644 index 0000000000000..0f8ae8e30c0e9 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -0,0 +1,131 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AppMenuAction, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; +import { omit } from 'lodash'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { DiscoverStateContainer } from '../../../state_management/discover_state'; +import { getSharingData, showPublicUrlSwitch } from '../../../../../utils/get_sharing_data'; +import { DiscoverAppLocatorParams } from '../../../../../../common/app_locator'; +import { DiscoverServices } from '../../../../../build_services'; + +export const getShareAppMenuItem = ({ + stateContainer, + services, +}: { + stateContainer: DiscoverStateContainer; + services: DiscoverServices; +}): AppMenuAction => { + return { + id: AppMenuActionId.share, + type: AppMenuActionType.secondary, + controlProps: { + label: i18n.translate('discover.localMenu.shareTitle', { + defaultMessage: 'Share', + }), + description: i18n.translate('discover.localMenu.shareSearchDescription', { + defaultMessage: 'Share Search', + }), + testId: 'shareTopNavButton', + onClick: async ({ getDiscoverParams, anchorElement }) => { + const { dataView, isEsqlMode } = getDiscoverParams(); + + if (!services.share) { + return; + } + + const savedSearch = stateContainer.savedSearchState.getState(); + const searchSourceSharingData = await getSharingData( + savedSearch.searchSource, + stateContainer.appState.getState(), + services, + isEsqlMode + ); + + const { locator, notifications } = services; + const appState = stateContainer.appState.getState(); + const { timefilter } = services.data.query.timefilter; + const timeRange = timefilter.getTime(); + const refreshInterval = timefilter.getRefreshInterval(); + const filters = services.filterManager.getFilters(); + + // Share -> Get links -> Snapshot + const params: DiscoverAppLocatorParams = { + ...omit(appState, 'dataSource'), + ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), + ...(dataView?.isPersisted() + ? { dataViewId: dataView?.id } + : { dataViewSpec: dataView?.toMinimalSpec() }), + filters, + timeRange, + refreshInterval, + }; + const relativeUrl = locator.getRedirectUrl(params); + + // This logic is duplicated from `relativeToAbsolute` (for bundle size reasons). Ultimately, this should be + // replaced when https://github.com/elastic/kibana/issues/153323 is implemented. + const link = document.createElement('a'); + link.setAttribute('href', relativeUrl); + const shareableUrl = link.href; + + // Share -> Get links -> Saved object + let shareableUrlForSavedObject = await locator.getUrl( + { savedSearchId: savedSearch.id }, + { absolute: true } + ); + + // UrlPanelContent forces a '_g' parameter in the saved object URL: + // https://github.com/elastic/kibana/blob/a30508153c1467b1968fb94faf1debc5407f61ea/src/plugins/share/public/components/url_panel_content.tsx#L230 + // Since our locator doesn't add the '_g' parameter if it's not needed, UrlPanelContent + // will interpret it as undefined and add '?_g=' to the URL, which is invalid in Discover, + // so instead we add an empty object for the '_g' parameter to the URL. + shareableUrlForSavedObject = setStateToKbnUrl( + '_g', + {}, + undefined, + shareableUrlForSavedObject + ); + + services.share.toggleShareContextMenu({ + anchorElement, + allowEmbed: false, + allowShortUrl: !!services.capabilities.discover.createShortUrl, + shareableUrl, + shareableUrlForSavedObject, + shareableUrlLocatorParams: { locator, params }, + objectId: savedSearch.id, + objectType: 'search', + objectTypeMeta: { + title: i18n.translate('discover.share.shareModal.title', { + defaultMessage: 'Share this search', + }), + }, + sharingData: { + isTextBased: isEsqlMode, + locatorParams: [{ id: locator.id, params }], + ...searchSourceSharingData, + // CSV reports can be generated without a saved search so we provide a fallback title + title: + savedSearch.title || + i18n.translate('discover.localMenu.fallbackReportTitle', { + defaultMessage: 'Untitled discover search', + }), + }, + isDirty: !savedSearch.id || stateContainer.appState.hasChanged(), + showPublicUrlSwitch, + onClose: () => { + anchorElement?.focus(); + }, + toasts: notifications.toasts, + }); + }, + }, + }; +}; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts new file mode 100644 index 0000000000000..5f275ef8c7a5a --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts @@ -0,0 +1,14 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { getAlertsAppMenuItem } from './get_alerts'; +export { getNewSearchAppMenuItem } from './get_new_search'; +export { getOpenSearchAppMenuItem } from './get_open_search'; +export { getShareAppMenuItem } from './get_share'; +export { getInspectAppMenuItem } from './get_inspect'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 610096a5afc51..c2ebfd1d46e85 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -7,28 +7,25 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { - AppMenuAction, - AppMenuActionId, - AppMenuActionType, - AppMenuDiscoverParams, - AppMenuItem, -} from '@kbn/discover-utils'; +import { AppMenuDiscoverParams, AppMenuItem } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { getAlertsActionItem } from './get_action_alerts'; +import { + getAlertsAppMenuItem, + getNewSearchAppMenuItem, + getOpenSearchAppMenuItem, + getShareAppMenuItem, + getInspectAppMenuItem, +} from './app_menu_actions'; import type { TopNavCustomization } from '../../../../customizations'; import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; -import { runShareAction } from './run_share_action'; -import { OpenSearchPanel } from './open_search_panel'; /** * Helper function to build the top nav links @@ -52,23 +49,6 @@ export const getTopNavLinks = ({ topNavCustomization: TopNavCustomization | undefined; shouldShowESQLToDataViewTransitionModal: boolean; }): TopNavMenuData[] => { - const getDiscoverParams = (): AppMenuDiscoverParams => ({ - dataView, - adHocDataViews, - isEsqlMode, - services, - savedSearch: state.savedSearchState.getState(), - savedQueryId: state.appState.getState().savedQuery, - query: state.appState.getState().query, - onUpdateAdHocDataViews: async (adHocDataViewList) => { - await state.actions.loadDataViewList(); - state.internalState.transitions.setAdHocDataViews(adHocDataViewList); - }, - }); - - const alertsItem = getAlertsActionItem({ getDiscoverParams }); - const alerts = convertMenuItem({ appMenuItem: alertsItem, getDiscoverParams }); - /** * Switches from ES|QL to classic mode and vice versa */ @@ -117,24 +97,6 @@ export const getTopNavLinks = ({ testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', }; - const newSearchItem: AppMenuAction = { - id: AppMenuActionId.new, - type: AppMenuActionType.secondary, // TODO: convert to primary - controlProps: { - label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { - defaultMessage: 'New', - }), - description: i18n.translate('discover.localMenu.newSearchDescription', { - defaultMessage: 'New Search', - }), - testId: 'discoverNewButton', - onClick: () => { - services.locator.navigate({}); - }, - }, - }; - const newSearch = convertMenuItem({ appMenuItem: newSearchItem, getDiscoverParams }); - const saveSearch = { id: 'save', label: i18n.translate('discover.localMenu.saveTitle', { @@ -158,66 +120,56 @@ export const getTopNavLinks = ({ }, }; - const openSearchItem: AppMenuAction = { - id: AppMenuActionId.open, - type: AppMenuActionType.secondary, // TODO: convert to primary - controlProps: { - label: i18n.translate('discover.localMenu.openTitle', { - defaultMessage: 'Open', - }), - description: i18n.translate('discover.localMenu.openSavedSearchDescription', { - defaultMessage: 'Open Saved Search', - }), - testId: 'discoverOpenButton', - onClick: ({ onFinishAction }) => { - return ( - - ); - }, + const getDiscoverParams = (): AppMenuDiscoverParams => ({ + dataView, + adHocDataViews, + isEsqlMode, + services, + savedSearch: state.savedSearchState.getState(), + savedQueryId: state.appState.getState().savedQuery, + query: state.appState.getState().query, + onUpdateAdHocDataViews: async (adHocDataViewList) => { + await state.actions.loadDataViewList(); + state.internalState.transitions.setAdHocDataViews(adHocDataViewList); }, - }; - const openSearch = convertMenuItem({ appMenuItem: openSearchItem, getDiscoverParams }); - - const shareSearch = { - id: 'share', - label: i18n.translate('discover.localMenu.shareTitle', { - defaultMessage: 'Share', - }), - description: i18n.translate('discover.localMenu.shareSearchDescription', { - defaultMessage: 'Share Search', - }), - testId: 'shareTopNavButton', - run: async (anchorElement: HTMLElement) => { - await runShareAction({ - anchorElement, - dataView, - stateContainer: state, - services, - isEsqlMode, - }); + onNewSearch: () => { + services.locator.navigate({}); }, - }; + onOpenSavedSearch: state.actions.onOpenSavedSearch, + }); - const inspectSearchItem: AppMenuAction = { - id: AppMenuActionId.inspect, - type: AppMenuActionType.secondary, - controlProps: { - label: i18n.translate('discover.localMenu.inspectTitle', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', { - defaultMessage: 'Open Inspector for search', - }), - testId: 'openInspectorButton', - onClick: () => { - onOpenInspector(); - }, - }, - }; - const inspectSearch = convertMenuItem({ appMenuItem: inspectSearchItem, getDiscoverParams }); + /* Primary items */ + + const newSearch = convertMenuItem({ + appMenuItem: getNewSearchAppMenuItem(), + getDiscoverParams, + }); + + const openSearch = convertMenuItem({ + appMenuItem: getOpenSearchAppMenuItem(), + getDiscoverParams, + }); + + const shareSearch = convertMenuItem({ + appMenuItem: getShareAppMenuItem({ stateContainer: state, services }), + getDiscoverParams, + }); + + /* Secondary items */ + + const alerts = convertMenuItem({ + appMenuItem: getAlertsAppMenuItem({ getDiscoverParams }), + getDiscoverParams, + }); + // TODO: allow to extend the alerts menu + + const inspectSearch = convertMenuItem({ + appMenuItem: getInspectAppMenuItem({ onOpenInspector }), + getDiscoverParams, + }); + + /* Custom items */ + // TODO: allow to extend with custom items const defaultMenu = topNavCustomization?.defaultMenu; const entries = [...(topNavCustomization?.getMenuItems?.() ?? [])]; @@ -226,16 +178,8 @@ export const getTopNavLinks = ({ entries.push({ data: esqLDataViewTransitionToggle, order: 0 }); } - if (!defaultMenu?.newItem?.disabled) { - entries.push({ data: newSearch, order: defaultMenu?.newItem?.order ?? 100 }); - } - - if (!defaultMenu?.openItem?.disabled) { - entries.push({ data: openSearch, order: defaultMenu?.openItem?.order ?? 200 }); - } - - if (!defaultMenu?.shareItem?.disabled) { - entries.push({ data: shareSearch, order: defaultMenu?.shareItem?.order ?? 300 }); + if (!defaultMenu?.inspectItem?.disabled) { + entries.push({ data: inspectSearch, order: defaultMenu?.inspectItem?.order ?? 100 }); } if ( @@ -243,11 +187,19 @@ export const getTopNavLinks = ({ services.capabilities.management?.insightsAndAlerting?.triggersActions && !defaultMenu?.alertsItem?.disabled ) { - entries.push({ data: alerts, order: defaultMenu?.alertsItem?.order ?? 400 }); + entries.push({ data: alerts, order: defaultMenu?.alertsItem?.order ?? 200 }); } - if (!defaultMenu?.inspectItem?.disabled) { - entries.push({ data: inspectSearch, order: defaultMenu?.inspectItem?.order ?? 500 }); + if (!defaultMenu?.newItem?.disabled) { + entries.push({ data: newSearch, order: defaultMenu?.newItem?.order ?? 300 }); + } + + if (!defaultMenu?.openItem?.disabled) { + entries.push({ data: openSearch, order: defaultMenu?.openItem?.order ?? 400 }); + } + + if (!defaultMenu?.shareItem?.disabled) { + entries.push({ data: shareSearch, order: defaultMenu?.shareItem?.order ?? 500 }); } if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) { diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts b/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts deleted file mode 100644 index 17a08d43cdddd..0000000000000 --- a/src/plugins/discover/public/application/main/components/top_nav/run_share_action.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { omit } from 'lodash'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { DiscoverServices } from '../../../../build_services'; -import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data'; -import { DiscoverAppLocatorParams } from '../../../../../common/app_locator'; - -export async function runShareAction({ - anchorElement, - dataView, - stateContainer, - services, - isEsqlMode, -}: { - anchorElement: HTMLElement; - dataView: DataView | undefined; - stateContainer: DiscoverStateContainer; - services: DiscoverServices; - isEsqlMode?: boolean; -}) { - if (!services.share) { - return; - } - const savedSearch = stateContainer.savedSearchState.getState(); - const searchSourceSharingData = await getSharingData( - savedSearch.searchSource, - stateContainer.appState.getState(), - services, - isEsqlMode - ); - - const { locator, notifications } = services; - const appState = stateContainer.appState.getState(); - const { timefilter } = services.data.query.timefilter; - const timeRange = timefilter.getTime(); - const refreshInterval = timefilter.getRefreshInterval(); - const filters = services.filterManager.getFilters(); - - // Share -> Get links -> Snapshot - const params: DiscoverAppLocatorParams = { - ...omit(appState, 'dataSource'), - ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), - ...(dataView?.isPersisted() - ? { dataViewId: dataView?.id } - : { dataViewSpec: dataView?.toMinimalSpec() }), - filters, - timeRange, - refreshInterval, - }; - const relativeUrl = locator.getRedirectUrl(params); - - // This logic is duplicated from `relativeToAbsolute` (for bundle size reasons). Ultimately, this should be - // replaced when https://github.com/elastic/kibana/issues/153323 is implemented. - const link = document.createElement('a'); - link.setAttribute('href', relativeUrl); - const shareableUrl = link.href; - - // Share -> Get links -> Saved object - let shareableUrlForSavedObject = await locator.getUrl( - { savedSearchId: savedSearch.id }, - { absolute: true } - ); - - // UrlPanelContent forces a '_g' parameter in the saved object URL: - // https://github.com/elastic/kibana/blob/a30508153c1467b1968fb94faf1debc5407f61ea/src/plugins/share/public/components/url_panel_content.tsx#L230 - // Since our locator doesn't add the '_g' parameter if it's not needed, UrlPanelContent - // will interpret it as undefined and add '?_g=' to the URL, which is invalid in Discover, - // so instead we add an empty object for the '_g' parameter to the URL. - shareableUrlForSavedObject = setStateToKbnUrl('_g', {}, undefined, shareableUrlForSavedObject); - - services.share.toggleShareContextMenu({ - anchorElement, - allowEmbed: false, - allowShortUrl: !!services.capabilities.discover.createShortUrl, - shareableUrl, - shareableUrlForSavedObject, - shareableUrlLocatorParams: { locator, params }, - objectId: savedSearch.id, - objectType: 'search', - objectTypeMeta: { - title: i18n.translate('discover.share.shareModal.title', { - defaultMessage: 'Share this search', - }), - }, - sharingData: { - isTextBased: isEsqlMode, - locatorParams: [{ id: locator.id, params }], - ...searchSourceSharingData, - // CSV reports can be generated without a saved search so we provide a fallback title - title: - savedSearch.title || - i18n.translate('discover.localMenu.fallbackReportTitle', { - defaultMessage: 'Untitled discover search', - }), - }, - isDirty: !savedSearch.id || stateContainer.appState.hasChanged(), - showPublicUrlSwitch, - onClose: () => { - anchorElement?.focus(); - }, - toasts: notifications.toasts, - }); -} From 80bcf51d3f8d74f7547048a0c88a6ea034acf766 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:00:20 +0000 Subject: [PATCH 05/61] [CI] Auto-commit changed files from 'node scripts/notice' --- packages/kbn-discover-utils/tsconfig.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index 01e0a315d0935..22e8dcaf09343 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -28,6 +28,12 @@ "@kbn/ui-theme", "@kbn/expressions-plugin", "@kbn/logs-data-access-plugin", - "@kbn/navigation-plugin" + "@kbn/navigation-plugin", + "@kbn/core-application-browser", + "@kbn/triggers-actions-ui-plugin", + "@kbn/saved-search-plugin", + "@kbn/core-lifecycle-browser", + "@kbn/data-plugin", + "@kbn/core-capabilities-common" ] } From 0197f7aad727a04d68078eb9a15766ff90c93efe Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 10:38:28 +0200 Subject: [PATCH 06/61] [Discover] Fix test ids --- .../main/components/top_nav/get_top_nav_links.tsx | 4 ++-- .../main/components/top_nav/run_app_menu_action.tsx | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index c2ebfd1d46e85..ef1a513b86a87 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -228,7 +228,7 @@ function convertMenuItem({ getDiscoverParams, }); }, - testId: appMenuItem.id, + testId: appMenuItem.testId, }; } @@ -243,6 +243,6 @@ function convertMenuItem({ getDiscoverParams, }); }, - testId: appMenuItem.id, + testId: appMenuItem.controlProps.testId, }; } diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx index f8c583afca100..0f528af81cbcd 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx @@ -96,12 +96,7 @@ export const AppMenuActionsMenuPopover: React.FC isOpen={!nestedContent} panelPaddingSize="s" > - + ); From 8fbc4dae50d5dce85a4e2df456b2e0d4a2240a2d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 13:04:55 +0200 Subject: [PATCH 07/61] [Discover] Fix circular deps --- .../services/saved_searches/to_saved_search.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/plugins/saved_search/public/services/saved_searches/to_saved_search.test.ts b/src/plugins/saved_search/public/services/saved_searches/to_saved_search.test.ts index 5b5c221ab9b1d..defb0e1a79986 100644 --- a/src/plugins/saved_search/public/services/saved_searches/to_saved_search.test.ts +++ b/src/plugins/saved_search/public/services/saved_searches/to_saved_search.test.ts @@ -10,7 +10,6 @@ import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils'; import { AttributeService, type EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { SavedSearchByValueAttributes, byValueToSavedSearch } from '.'; @@ -21,12 +20,7 @@ const mockServices = { spaces: spacesPluginMock.createStartContract(), embeddable: { getAttributeService: jest.fn( - (_, opts) => - new AttributeService( - SEARCH_EMBEDDABLE_TYPE, - coreMock.createStart().notifications.toasts, - opts - ) + (_, opts) => new AttributeService('search', coreMock.createStart().notifications.toasts, opts) ), } as unknown as EmbeddableStart, }; From b3cab7ce68cf545f4aff8f98a1266dc37d7aa063 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:17:57 +0000 Subject: [PATCH 08/61] [CI] Auto-commit changed files from 'node scripts/notice' --- src/plugins/saved_search/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/saved_search/tsconfig.json b/src/plugins/saved_search/tsconfig.json index f96d7b385aa2c..803e2b010d952 100644 --- a/src/plugins/saved_search/tsconfig.json +++ b/src/plugins/saved_search/tsconfig.json @@ -27,7 +27,6 @@ "@kbn/core-plugins-server", "@kbn/utility-types", "@kbn/search-types", - "@kbn/discover-utils", "@kbn/unified-data-table", ], "exclude": ["target/**/*"] From 37f5ce302a9b6a8fad228c5096c349e9b15c75d6 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 14:54:52 +0200 Subject: [PATCH 09/61] [Discover] Fix circular deps --- .../src/components/app_menu/types.ts | 7 ----- .../top_nav/app_menu_actions/get_alerts.tsx | 29 ++++++++++--------- .../app_menu_actions/get_new_search.tsx | 9 ++++-- .../app_menu_actions/get_open_search.tsx | 9 ++++-- .../components/top_nav/get_top_nav_links.tsx | 17 +++++------ 5 files changed, 34 insertions(+), 37 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 354b511db5b86..c9b1064c6a2af 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -10,10 +10,8 @@ import React from 'react'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; -import type { AggregateQuery, Query } from '@kbn/es-query'; import type { ApplicationStart } from '@kbn/core-application-browser'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; -import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public'; import type { Capabilities } from '@kbn/core-capabilities-common'; @@ -22,9 +20,6 @@ export interface AppMenuDiscoverParams { dataView: DataView | undefined; adHocDataViews: DataView[]; isEsqlMode?: boolean; - query?: Query | AggregateQuery; - savedQueryId?: string; - savedSearch: SavedSearch; services: { core: CoreStart; application: ApplicationStart; @@ -34,8 +29,6 @@ export interface AppMenuDiscoverParams { capabilities: Capabilities; }; onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; - onNewSearch: () => void; - onOpenSavedSearch: (id: string) => void; } export interface AppMenuControlOnClickParams { diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index d91ba7e485e38..cb9bfc1440019 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -23,6 +23,7 @@ import { STACK_ALERTS_FEATURE_ID, } from '@kbn/rule-data-utils'; import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; +import { DiscoverStateContainer } from '../../../state_management/discover_state'; const EsQueryValidConsumer: RuleCreationValidConsumer[] = [ AlertConsumers.INFRASTRUCTURE, @@ -39,17 +40,12 @@ interface EsQueryAlertMetaData extends RuleTypeMetaData { const CreateAlertFlyout: React.FC<{ getDiscoverParams: () => AppMenuDiscoverParams; onFinishAction: () => void; -}> = React.memo(({ getDiscoverParams, onFinishAction }) => { - const { - dataView, - services, - savedQueryId, - savedSearch, - isEsqlMode, - query, - adHocDataViews, - onUpdateAdHocDataViews, - } = getDiscoverParams(); + stateContainer: DiscoverStateContainer; +}> = React.memo(({ stateContainer, getDiscoverParams, onFinishAction }) => { + const query = stateContainer.appState.getState().query; + + const { dataView, services, isEsqlMode, adHocDataViews, onUpdateAdHocDataViews } = + getDiscoverParams(); const { triggersActionsUi } = services; const timeField = getTimeField(dataView); @@ -64,12 +60,15 @@ const CreateAlertFlyout: React.FC<{ timeField, }; } + const savedQueryId = stateContainer.appState.getState().savedQuery; return { searchType: 'searchSource', - searchConfiguration: savedSearch.searchSource.getSerializedFields(), + searchConfiguration: stateContainer.savedSearchState + .getState() + .searchSource.getSerializedFields(), savedQueryId, }; - }, [isEsqlMode, savedSearch, savedQueryId, query, timeField]); + }, [isEsqlMode, stateContainer.appState, stateContainer.savedSearchState, query, timeField]); const discoverMetadata: EsQueryAlertMetaData = useMemo( () => ({ @@ -100,9 +99,11 @@ const CreateAlertFlyout: React.FC<{ }); export const getAlertsAppMenuItem = ({ + stateContainer, getDiscoverParams, }: { getDiscoverParams: () => AppMenuDiscoverParams; + stateContainer: DiscoverStateContainer; }): AppMenuPopoverActions => { const { dataView, services, isEsqlMode } = getDiscoverParams(); const timeField = getTimeField(dataView); @@ -134,7 +135,7 @@ export const getAlertsAppMenuItem = ({ defaultMessage: 'Data view does not have a time field.', }), onClick: async (params) => { - return ; + return ; }, }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index 3efe874c362d7..1c1f846511efa 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -10,7 +10,11 @@ import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; -export const getNewSearchAppMenuItem = (): AppMenuAction => { +export const getNewSearchAppMenuItem = ({ + onNewSearch, +}: { + onNewSearch: () => void; +}): AppMenuAction => { return { id: AppMenuActionId.new, type: AppMenuActionType.secondary, // TODO: convert to primary @@ -22,8 +26,7 @@ export const getNewSearchAppMenuItem = (): AppMenuAction => { defaultMessage: 'New Search', }), testId: 'discoverNewButton', - onClick: ({ getDiscoverParams }) => { - const { onNewSearch } = getDiscoverParams(); + onClick: () => { onNewSearch(); }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index d1c0faf059f38..3dc2137385ff7 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -12,7 +12,11 @@ import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; -export const getOpenSearchAppMenuItem = (): AppMenuAction => { +export const getOpenSearchAppMenuItem = ({ + onOpenSavedSearch, +}: { + onOpenSavedSearch: (id: string) => void; +}): AppMenuAction => { return { id: AppMenuActionId.open, type: AppMenuActionType.secondary, // TODO: convert to primary @@ -24,8 +28,7 @@ export const getOpenSearchAppMenuItem = (): AppMenuAction => { defaultMessage: 'Open Saved Search', }), testId: 'discoverOpenButton', - onClick: ({ getDiscoverParams, onFinishAction }) => { - const { onOpenSavedSearch } = getDiscoverParams(); + onClick: ({ onFinishAction }) => { return ; }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index ef1a513b86a87..f5930e9bed8cd 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -125,28 +125,25 @@ export const getTopNavLinks = ({ adHocDataViews, isEsqlMode, services, - savedSearch: state.savedSearchState.getState(), - savedQueryId: state.appState.getState().savedQuery, - query: state.appState.getState().query, onUpdateAdHocDataViews: async (adHocDataViewList) => { await state.actions.loadDataViewList(); state.internalState.transitions.setAdHocDataViews(adHocDataViewList); }, - onNewSearch: () => { - services.locator.navigate({}); - }, - onOpenSavedSearch: state.actions.onOpenSavedSearch, }); /* Primary items */ const newSearch = convertMenuItem({ - appMenuItem: getNewSearchAppMenuItem(), + appMenuItem: getNewSearchAppMenuItem({ + onNewSearch: () => { + services.locator.navigate({}); + }, + }), getDiscoverParams, }); const openSearch = convertMenuItem({ - appMenuItem: getOpenSearchAppMenuItem(), + appMenuItem: getOpenSearchAppMenuItem({ onOpenSavedSearch: state.actions.onOpenSavedSearch }), getDiscoverParams, }); @@ -158,7 +155,7 @@ export const getTopNavLinks = ({ /* Secondary items */ const alerts = convertMenuItem({ - appMenuItem: getAlertsAppMenuItem({ getDiscoverParams }), + appMenuItem: getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }), getDiscoverParams, }); // TODO: allow to extend the alerts menu From e4b0296e5633a3729e07fe38c7ed17185ecbbf84 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:07:25 +0000 Subject: [PATCH 10/61] [CI] Auto-commit changed files from 'node scripts/notice' --- packages/kbn-discover-utils/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index 22e8dcaf09343..dd20548a66574 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -31,7 +31,6 @@ "@kbn/navigation-plugin", "@kbn/core-application-browser", "@kbn/triggers-actions-ui-plugin", - "@kbn/saved-search-plugin", "@kbn/core-lifecycle-browser", "@kbn/data-plugin", "@kbn/core-capabilities-common" From 16756e5877e15d2b44259885ec7e04226987a712 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 25 Sep 2024 17:00:06 -0300 Subject: [PATCH 11/61] [Discover] Start using icons for the primary actions --- .../app_menu_actions/get_new_search.tsx | 7 +-- .../app_menu_actions/get_open_search.tsx | 7 +-- .../top_nav/app_menu_actions/get_share.tsx | 7 +-- .../components/top_nav/get_top_nav_links.tsx | 9 ++-- .../public/top_nav_menu/top_nav_menu_data.tsx | 1 + .../public/top_nav_menu/top_nav_menu_item.tsx | 43 +++++++++++++------ .../top_nav_menu/top_nav_menu_items.tsx | 14 ++++-- 7 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index 1c1f846511efa..ddc3c8ac32d04 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -7,17 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; export const getNewSearchAppMenuItem = ({ onNewSearch, }: { onNewSearch: () => void; -}): AppMenuAction => { +}): AppMenuIconAction => { return { id: AppMenuActionId.new, - type: AppMenuActionType.secondary, // TODO: convert to primary + type: AppMenuActionType.primary, controlProps: { label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { defaultMessage: 'New', @@ -25,6 +25,7 @@ export const getNewSearchAppMenuItem = ({ description: i18n.translate('discover.localMenu.newSearchDescription', { defaultMessage: 'New Search', }), + iconType: 'plus', testId: 'discoverNewButton', onClick: () => { onNewSearch(); diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index 3dc2137385ff7..fe7ce8568aef8 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; @@ -16,10 +16,10 @@ export const getOpenSearchAppMenuItem = ({ onOpenSavedSearch, }: { onOpenSavedSearch: (id: string) => void; -}): AppMenuAction => { +}): AppMenuIconAction => { return { id: AppMenuActionId.open, - type: AppMenuActionType.secondary, // TODO: convert to primary + type: AppMenuActionType.primary, controlProps: { label: i18n.translate('discover.localMenu.openTitle', { defaultMessage: 'Open', @@ -27,6 +27,7 @@ export const getOpenSearchAppMenuItem = ({ description: i18n.translate('discover.localMenu.openSavedSearchDescription', { defaultMessage: 'Open Saved Search', }), + iconType: 'folderOpen', testId: 'discoverOpenButton', onClick: ({ onFinishAction }) => { return ; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index 0f8ae8e30c0e9..ae4babcf36d56 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuAction, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; +import { AppMenuIconAction, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; import { omit } from 'lodash'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { i18n } from '@kbn/i18n'; @@ -22,10 +22,10 @@ export const getShareAppMenuItem = ({ }: { stateContainer: DiscoverStateContainer; services: DiscoverServices; -}): AppMenuAction => { +}): AppMenuIconAction => { return { id: AppMenuActionId.share, - type: AppMenuActionType.secondary, + type: AppMenuActionType.primary, controlProps: { label: i18n.translate('discover.localMenu.shareTitle', { defaultMessage: 'Share', @@ -33,6 +33,7 @@ export const getShareAppMenuItem = ({ description: i18n.translate('discover.localMenu.shareSearchDescription', { defaultMessage: 'Share Search', }), + iconType: 'link', testId: 'shareTopNavButton', onClick: async ({ getDiscoverParams, anchorElement }) => { const { dataView, isEsqlMode } = getDiscoverParams(); diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index f5930e9bed8cd..c096e872dad80 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -12,7 +12,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { AppMenuDiscoverParams, AppMenuItem } from '@kbn/discover-utils'; +import { AppMenuDiscoverParams, AppMenuItem, AppMenuActionType } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; @@ -218,6 +218,7 @@ function convertMenuItem({ id: appMenuItem.id, label: appMenuItem.label, description: appMenuItem.description ?? appMenuItem.label, + testId: appMenuItem.testId, run: (anchorElement: HTMLElement) => { runAppMenuPopoverAction({ appMenuItem, @@ -225,7 +226,6 @@ function convertMenuItem({ getDiscoverParams, }); }, - testId: appMenuItem.testId, }; } @@ -233,6 +233,7 @@ function convertMenuItem({ id: appMenuItem.id, label: appMenuItem.controlProps.label, description: appMenuItem.controlProps.description ?? appMenuItem.controlProps.label, + testId: appMenuItem.controlProps.testId, run: async (anchorElement: HTMLElement) => { await runAppMenuAction({ appMenuItem, @@ -240,6 +241,8 @@ function convertMenuItem({ getDiscoverParams, }); }, - testId: appMenuItem.controlProps.testId, + ...(appMenuItem.type === AppMenuActionType.primary + ? { iconType: appMenuItem.controlProps.iconType, iconOnly: true } + : {}), }; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index bcb8556f028a5..a399701dfc451 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -28,6 +28,7 @@ export interface TopNavMenuData { isLoading?: boolean; iconType?: string; iconSide?: EuiButtonProps['iconSide']; + iconOnly?: boolean; target?: string; href?: string; intl?: InjectedIntl; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index a9ef055d6e1fd..058c94732e272 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -9,10 +9,17 @@ import { upperFirst, isFunction } from 'lodash'; import React, { MouseEvent } from 'react'; -import { EuiToolTip, EuiButton, EuiHeaderLink, EuiBetaBadge, EuiButtonColor } from '@elastic/eui'; +import { + EuiToolTip, + EuiButton, + EuiHeaderLink, + EuiBetaBadge, + EuiButtonColor, + EuiButtonIcon, +} from '@elastic/eui'; import { TopNavMenuData } from './top_nav_menu_data'; -export function TopNavMenuItem(props: TopNavMenuData) { +export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean }) { function isDisabled(): boolean { const val = isFunction(props.disableButton) ? props.disableButton() : props.disableButton; return val!; @@ -59,16 +66,28 @@ export function TopNavMenuItem(props: TopNavMenuData) { ? { onClick: undefined, href: props.href, target: props.target } : {}; - // fill is not compatible with EuiHeaderLink - const btn = props.emphasize ? ( - - {getButtonContainer()} - - ) : ( - - {getButtonContainer()} - - ); + const btn = + props.iconOnly && props.iconType && !props.isMobileMenu ? ( + // icon only buttons are not supported by EuiHeaderLink + + + + ) : props.emphasize ? ( + // fill is not compatible with EuiHeaderLink + + {getButtonContainer()} + + ) : ( + + {getButtonContainer()} + + ); const tooltip = getTooltip(); if (tooltip) { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx index 48179f30ec276..20cd794f7a056 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx @@ -7,11 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiHeaderLinks } from '@elastic/eui'; +import { EuiHeaderLinks, useIsWithinBreakpoints } from '@elastic/eui'; import React from 'react'; import type { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; +const POPOVER_BREAKPOINTS = ['xs', 's']; + export const TopNavMenuItems = ({ config, className, @@ -19,11 +21,17 @@ export const TopNavMenuItems = ({ config: TopNavMenuData[] | undefined; className?: string; }) => { + const isMobileMenu = useIsWithinBreakpoints(POPOVER_BREAKPOINTS); if (!config || config.length === 0) return null; return ( - + {config.map((menuItem: TopNavMenuData, i: number) => { - return ; + return ; })} ); From 0c89b3e331038f4944e5a8c0b0e976f1360e5d1c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 16:44:03 +0200 Subject: [PATCH 12/61] [Discover] Avoid the circular deps --- .../src/components/app_menu/types.ts | 22 ---- .../convert_to_top_nav_item.ts | 54 ++++++++ .../get_alerts.test.tsx} | 44 ++++--- .../top_nav/app_menu_actions/get_alerts.tsx | 16 +-- .../app_menu_actions/get_new_search.tsx | 6 +- .../app_menu_actions/get_open_search.tsx | 6 +- .../top_nav/app_menu_actions/get_share.tsx | 10 +- .../top_nav/app_menu_actions/index.ts | 2 + .../run_app_menu_action.tsx | 25 ++-- .../top_nav/app_menu_actions/types.ts | 21 ++++ .../top_nav/discover_topnav.test.tsx | 6 +- .../top_nav/get_top_nav_links.test.ts | 117 ++++++++++-------- .../components/top_nav/get_top_nav_links.tsx | 77 +++--------- 13 files changed, 224 insertions(+), 182 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.ts rename src/plugins/discover/public/application/main/components/top_nav/{open_alerts_popover.test.tsx => app_menu_actions/get_alerts.test.tsx} (73%) rename src/plugins/discover/public/application/main/components/top_nav/{ => app_menu_actions}/run_app_menu_action.tsx (89%) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index c9b1064c6a2af..8ad14d4bf3d79 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -9,31 +9,9 @@ import React from 'react'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { ApplicationStart } from '@kbn/core-application-browser'; -import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; -import type { CoreStart } from '@kbn/core-lifecycle-browser'; -import type { DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public'; -import type { Capabilities } from '@kbn/core-capabilities-common'; - -export interface AppMenuDiscoverParams { - dataView: DataView | undefined; - adHocDataViews: DataView[]; - isEsqlMode?: boolean; - services: { - core: CoreStart; - application: ApplicationStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; - data: DataPublicPluginStart; - filterManager: FilterManager; - capabilities: Capabilities; - }; - onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; -} export interface AppMenuControlOnClickParams { anchorElement: HTMLElement; - getDiscoverParams: () => AppMenuDiscoverParams; onFinishAction: () => void; } diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.ts new file mode 100644 index 0000000000000..2ff2d531d77cf --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.ts @@ -0,0 +1,54 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AppMenuActionType, AppMenuItem } from '@kbn/discover-utils'; +import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; +import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; +import { DiscoverServices } from '../../../../../build_services'; + +export function convertAppMenuItemToTopNavItem({ + appMenuItem, + services, +}: { + appMenuItem: AppMenuItem; + services: DiscoverServices; +}): TopNavMenuData { + if ('actions' in appMenuItem) { + return { + id: appMenuItem.id, + label: appMenuItem.label, + description: appMenuItem.description ?? appMenuItem.label, + testId: appMenuItem.testId, + run: (anchorElement: HTMLElement) => { + runAppMenuPopoverAction({ + appMenuItem, + anchorElement, + services, + }); + }, + }; + } + + return { + id: appMenuItem.id, + label: appMenuItem.controlProps.label, + description: appMenuItem.controlProps.description ?? appMenuItem.controlProps.label, + testId: appMenuItem.controlProps.testId, + run: async (anchorElement: HTMLElement) => { + await runAppMenuAction({ + appMenuItem, + anchorElement, + services, + }); + }, + ...(appMenuItem.type === AppMenuActionType.primary + ? { iconType: appMenuItem.controlProps.iconType, iconOnly: true } + : {}), + }; +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.test.tsx similarity index 73% rename from src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx rename to src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.test.tsx index fb9f127b83d86..7bfee2d3f2d59 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.test.tsx @@ -10,28 +10,40 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { AlertsPopover } from './open_alerts_popover'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; -import { dataViewWithNoTimefieldMock } from '../../../../__mocks__/data_view_no_timefield'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; +import { AppMenuActionsMenuPopover } from './run_app_menu_action'; +import { getAlertsAppMenuItem } from './get_alerts'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; +import { dataViewWithTimefieldMock } from '../../../../../__mocks__/data_view_with_timefield'; +import { dataViewWithNoTimefieldMock } from '../../../../../__mocks__/data_view_no_timefield'; +import { getDiscoverStateMock } from '../../../../../__mocks__/discover_state.mock'; const mount = (dataView = dataViewMock, isEsqlMode = false) => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); stateContainer.actions.setDataView(dataView); + + const getDiscoverParamsMock = () => ({ + dataView, + adHocDataViews: [], + isEsqlMode, + services: discoverServiceMock, + onNewSearch: jest.fn(), + onOpenSavedSearch: jest.fn(), + onUpdateAdHocDataViews: jest.fn(), + }); + + const alertsAppMenuItem = getAlertsAppMenuItem({ + getDiscoverParams: getDiscoverParamsMock, + stateContainer, + }); + return mountWithIntl( - - - + ); }; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index cb9bfc1440019..01e6727bfdbf6 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -10,12 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; -import { - AppMenuActionId, - AppMenuActionType, - AppMenuDiscoverParams, - AppMenuPopoverActions, -} from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuPopoverActions } from '@kbn/discover-utils'; import { AlertConsumers, ES_QUERY_ID, @@ -24,6 +19,7 @@ import { } from '@kbn/rule-data-utils'; import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; import { DiscoverStateContainer } from '../../../state_management/discover_state'; +import { AppMenuDiscoverParams } from './types'; const EsQueryValidConsumer: RuleCreationValidConsumer[] = [ AlertConsumers.INFRASTRUCTURE, @@ -135,7 +131,13 @@ export const getAlertsAppMenuItem = ({ defaultMessage: 'Data view does not have a time field.', }), onClick: async (params) => { - return ; + return ( + + ); }, }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index ddc3c8ac32d04..d857e1f1f945c 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -9,11 +9,12 @@ import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; +import { AppMenuDiscoverParams } from './types'; export const getNewSearchAppMenuItem = ({ - onNewSearch, + getDiscoverParams, }: { - onNewSearch: () => void; + getDiscoverParams: () => AppMenuDiscoverParams; }): AppMenuIconAction => { return { id: AppMenuActionId.new, @@ -28,6 +29,7 @@ export const getNewSearchAppMenuItem = ({ iconType: 'plus', testId: 'discoverNewButton', onClick: () => { + const { onNewSearch } = getDiscoverParams(); onNewSearch(); }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index fe7ce8568aef8..e986ab969e881 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -11,11 +11,12 @@ import React from 'react'; import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; +import { AppMenuDiscoverParams } from './types'; export const getOpenSearchAppMenuItem = ({ - onOpenSavedSearch, + getDiscoverParams, }: { - onOpenSavedSearch: (id: string) => void; + getDiscoverParams: () => AppMenuDiscoverParams; }): AppMenuIconAction => { return { id: AppMenuActionId.open, @@ -30,6 +31,7 @@ export const getOpenSearchAppMenuItem = ({ iconType: 'folderOpen', testId: 'discoverOpenButton', onClick: ({ onFinishAction }) => { + const { onOpenSavedSearch } = getDiscoverParams(); return ; }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index ae4babcf36d56..616dc91302ac4 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -14,14 +14,14 @@ import { i18n } from '@kbn/i18n'; import { DiscoverStateContainer } from '../../../state_management/discover_state'; import { getSharingData, showPublicUrlSwitch } from '../../../../../utils/get_sharing_data'; import { DiscoverAppLocatorParams } from '../../../../../../common/app_locator'; -import { DiscoverServices } from '../../../../../build_services'; +import { AppMenuDiscoverParams } from './types'; export const getShareAppMenuItem = ({ stateContainer, - services, + getDiscoverParams, }: { stateContainer: DiscoverStateContainer; - services: DiscoverServices; + getDiscoverParams: () => AppMenuDiscoverParams; }): AppMenuIconAction => { return { id: AppMenuActionId.share, @@ -35,8 +35,8 @@ export const getShareAppMenuItem = ({ }), iconType: 'link', testId: 'shareTopNavButton', - onClick: async ({ getDiscoverParams, anchorElement }) => { - const { dataView, isEsqlMode } = getDiscoverParams(); + onClick: async ({ anchorElement }) => { + const { dataView, isEsqlMode, services } = getDiscoverParams(); if (!services.share) { return; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts index 5f275ef8c7a5a..6a5c2f31946a2 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/index.ts @@ -12,3 +12,5 @@ export { getNewSearchAppMenuItem } from './get_new_search'; export { getOpenSearchAppMenuItem } from './get_open_search'; export { getShareAppMenuItem } from './get_share'; export { getInspectAppMenuItem } from './get_inspect'; +export { convertAppMenuItemToTopNavItem } from './convert_to_top_nav_item'; +export * from './types'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx similarity index 89% rename from src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx rename to src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx index 0f528af81cbcd..5c52aa688b688 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx @@ -21,12 +21,8 @@ import ReactDOM from 'react-dom'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import type { - AppMenuAction, - AppMenuDiscoverParams, - AppMenuIconAction, - AppMenuPopoverActions, -} from '@kbn/discover-utils'; +import type { AppMenuAction, AppMenuIconAction, AppMenuPopoverActions } from '@kbn/discover-utils'; +import type { DiscoverServices } from '../../../../../build_services'; const container = document.createElement('div'); let isOpen = false; @@ -34,14 +30,13 @@ let isOpen = false; interface AppMenuActionsMenuPopoverProps { appMenuItem: AppMenuPopoverActions; anchorElement: HTMLElement; - getDiscoverParams: () => AppMenuDiscoverParams; + services: DiscoverServices; onClose: () => void; } export const AppMenuActionsMenuPopover: React.FC = ({ appMenuItem, anchorElement, - getDiscoverParams, onClose: originalOnClose, }) => { const [nestedContent, setNestedContent] = useState(); @@ -67,7 +62,6 @@ export const AppMenuActionsMenuPopover: React.FC onClick: async () => { const result = await controlProps.onClick?.({ anchorElement, - getDiscoverParams, onFinishAction: onClose, }); @@ -114,11 +108,11 @@ function cleanup() { export function runAppMenuPopoverAction({ appMenuItem, anchorElement, - getDiscoverParams, + services, }: { appMenuItem: AppMenuPopoverActions; anchorElement: HTMLElement; - getDiscoverParams: () => AppMenuDiscoverParams; + services: DiscoverServices; }) { if (isOpen) { cleanup(); @@ -127,7 +121,6 @@ export function runAppMenuPopoverAction({ isOpen = true; document.body.appendChild(container); - const { services } = getDiscoverParams(); const element = ( @@ -135,7 +128,7 @@ export function runAppMenuPopoverAction({ @@ -147,20 +140,18 @@ export function runAppMenuPopoverAction({ export async function runAppMenuAction({ appMenuItem, anchorElement, - getDiscoverParams, + services, }: { appMenuItem: AppMenuAction | AppMenuIconAction; anchorElement: HTMLElement; - getDiscoverParams: () => AppMenuDiscoverParams; + services: DiscoverServices; }) { cleanup(); - const { services } = getDiscoverParams(); const controlProps = appMenuItem.controlProps; const result = await controlProps.onClick?.({ anchorElement, - getDiscoverParams, onFinishAction: () => { cleanup(); anchorElement?.focus(); diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts new file mode 100644 index 0000000000000..cc5f98a916b78 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts @@ -0,0 +1,21 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DiscoverServices } from '../../../../../build_services'; + +export interface AppMenuDiscoverParams { + dataView: DataView | undefined; + adHocDataViews: DataView[]; + isEsqlMode?: boolean; + services: DiscoverServices; + onNewSearch: () => void; + onOpenSavedSearch: (id: string) => void; + onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; +} diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx index fcc5fb615e81a..d99cbd91ffd35 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx @@ -116,7 +116,7 @@ describe('Discover topnav component', () => { ); const topNavMenu = component.find(TopNavMenu); const topMenuConfig = topNavMenu.props().config?.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['new', 'open', 'share', 'inspect', 'save']); + expect(topMenuConfig).toEqual(['inspect', 'new', 'open', 'share', 'save']); }); test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => { @@ -128,7 +128,7 @@ describe('Discover topnav component', () => { ); const topNavMenu = component.find(TopNavMenu).props(); const topMenuConfig = topNavMenu.config?.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['new', 'open', 'share', 'inspect']); + expect(topMenuConfig).toEqual(['inspect', 'new', 'open', 'share']); }); test('top nav is correct when discover saveQuery permission is granted', () => { @@ -180,7 +180,7 @@ describe('Discover topnav component', () => { expect(mockTopNavCustomization.getMenuItems).toHaveBeenCalledTimes(1); const topNavMenu = component.find(TopNavMenu); const topMenuConfig = topNavMenu.props().config?.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['new', 'open', 'share', 'test', 'inspect', 'save']); + expect(topMenuConfig).toEqual(['inspect', 'new', 'test', 'open', 'share', 'save']); }); it('should allow disabling default menu items', () => { diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts index bdf29cf152ce7..48f10944fc1ed 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts @@ -10,33 +10,37 @@ import { getTopNavLinks } from './get_top_nav_links'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { DiscoverServices } from '../../../../build_services'; -import { DiscoverStateContainer } from '../../state_management/discover_state'; +import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; -const services = { - capabilities: { - discover: { - save: true, +describe('getTopNavLinks', () => { + const services = { + ...createDiscoverServicesMock(), + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: { + get: jest.fn(() => true), }, - }, - uiSettings: { - get: jest.fn(() => true), - }, -} as unknown as DiscoverServices; + } as unknown as DiscoverServices; -const state = {} as unknown as DiscoverStateContainer; + const state = getDiscoverStateMock({ isTimeBased: true }); + state.actions.setDataView(dataViewMock); -test('getTopNavLinks result', () => { - const topNavLinks = getTopNavLinks({ - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: false, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, - }); - expect(topNavLinks).toMatchInlineSnapshot(` + test('getTopNavLinks result', () => { + const topNavLinks = getTopNavLinks({ + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: false, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }); + expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { "color": "text", @@ -48,8 +52,17 @@ test('getTopNavLinks result', () => { "testId": "select-text-based-language-btn", "tooltip": "ES|QL is Elastic's powerful new piped query language.", }, + Object { + "description": "Open Inspector for search", + "id": "inspect", + "label": "Inspect", + "run": [Function], + "testId": "openInspectorButton", + }, Object { "description": "New Search", + "iconOnly": true, + "iconType": "plus", "id": "new", "label": "New", "run": [Function], @@ -57,6 +70,8 @@ test('getTopNavLinks result', () => { }, Object { "description": "Open Saved Search", + "iconOnly": true, + "iconType": "folderOpen", "id": "open", "label": "Open", "run": [Function], @@ -64,18 +79,13 @@ test('getTopNavLinks result', () => { }, Object { "description": "Share Search", + "iconOnly": true, + "iconType": "link", "id": "share", "label": "Share", "run": [Function], "testId": "shareTopNavButton", }, - Object { - "description": "Open Inspector for search", - "id": "inspect", - "label": "Inspect", - "run": [Function], - "testId": "openInspectorButton", - }, Object { "description": "Save Search", "emphasize": true, @@ -87,20 +97,20 @@ test('getTopNavLinks result', () => { }, ] `); -}); - -test('getTopNavLinks result for ES|QL mode', () => { - const topNavLinks = getTopNavLinks({ - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: true, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, }); - expect(topNavLinks).toMatchInlineSnapshot(` + + test('getTopNavLinks result for ES|QL mode', () => { + const topNavLinks = getTopNavLinks({ + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: true, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }); + expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { "color": "text", @@ -112,8 +122,17 @@ test('getTopNavLinks result for ES|QL mode', () => { "testId": "switch-to-dataviews", "tooltip": "Switch to KQL or Lucene syntax.", }, + Object { + "description": "Open Inspector for search", + "id": "inspect", + "label": "Inspect", + "run": [Function], + "testId": "openInspectorButton", + }, Object { "description": "New Search", + "iconOnly": true, + "iconType": "plus", "id": "new", "label": "New", "run": [Function], @@ -121,6 +140,8 @@ test('getTopNavLinks result for ES|QL mode', () => { }, Object { "description": "Open Saved Search", + "iconOnly": true, + "iconType": "folderOpen", "id": "open", "label": "Open", "run": [Function], @@ -128,18 +149,13 @@ test('getTopNavLinks result for ES|QL mode', () => { }, Object { "description": "Share Search", + "iconOnly": true, + "iconType": "link", "id": "share", "label": "Share", "run": [Function], "testId": "shareTopNavButton", }, - Object { - "description": "Open Inspector for search", - "id": "inspect", - "label": "Inspect", - "run": [Function], - "testId": "openInspectorButton", - }, Object { "description": "Save Search", "emphasize": true, @@ -151,4 +167,5 @@ test('getTopNavLinks result for ES|QL mode', () => { }, ] `); + }); }); diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index c096e872dad80..9c5c223dcd7df 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -12,7 +12,6 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { AppMenuDiscoverParams, AppMenuItem, AppMenuActionType } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; @@ -23,9 +22,10 @@ import { getOpenSearchAppMenuItem, getShareAppMenuItem, getInspectAppMenuItem, + convertAppMenuItemToTopNavItem, + AppMenuDiscoverParams, } from './app_menu_actions'; import type { TopNavCustomization } from '../../../../customizations'; -import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; /** * Helper function to build the top nav links @@ -129,40 +129,42 @@ export const getTopNavLinks = ({ await state.actions.loadDataViewList(); state.internalState.transitions.setAdHocDataViews(adHocDataViewList); }, + onNewSearch: () => { + services.locator.navigate({}); + }, + onOpenSavedSearch: state.actions.onOpenSavedSearch, }); /* Primary items */ - const newSearch = convertMenuItem({ + const newSearch = convertAppMenuItemToTopNavItem({ appMenuItem: getNewSearchAppMenuItem({ - onNewSearch: () => { - services.locator.navigate({}); - }, + getDiscoverParams, }), - getDiscoverParams, + services, }); - const openSearch = convertMenuItem({ - appMenuItem: getOpenSearchAppMenuItem({ onOpenSavedSearch: state.actions.onOpenSavedSearch }), - getDiscoverParams, + const openSearch = convertAppMenuItemToTopNavItem({ + appMenuItem: getOpenSearchAppMenuItem({ getDiscoverParams }), + services, }); - const shareSearch = convertMenuItem({ - appMenuItem: getShareAppMenuItem({ stateContainer: state, services }), - getDiscoverParams, + const shareSearch = convertAppMenuItemToTopNavItem({ + appMenuItem: getShareAppMenuItem({ getDiscoverParams, stateContainer: state }), + services, }); /* Secondary items */ - const alerts = convertMenuItem({ + const alerts = convertAppMenuItemToTopNavItem({ appMenuItem: getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }), - getDiscoverParams, + services, }); // TODO: allow to extend the alerts menu - const inspectSearch = convertMenuItem({ + const inspectSearch = convertAppMenuItemToTopNavItem({ appMenuItem: getInspectAppMenuItem({ onOpenInspector }), - getDiscoverParams, + services, }); /* Custom items */ @@ -205,44 +207,3 @@ export const getTopNavLinks = ({ return entries.sort((a, b) => a.order - b.order).map((entry) => entry.data); }; - -function convertMenuItem({ - appMenuItem, - getDiscoverParams, -}: { - appMenuItem: AppMenuItem; - getDiscoverParams: () => AppMenuDiscoverParams; -}): TopNavMenuData { - if ('actions' in appMenuItem) { - return { - id: appMenuItem.id, - label: appMenuItem.label, - description: appMenuItem.description ?? appMenuItem.label, - testId: appMenuItem.testId, - run: (anchorElement: HTMLElement) => { - runAppMenuPopoverAction({ - appMenuItem, - anchorElement, - getDiscoverParams, - }); - }, - }; - } - - return { - id: appMenuItem.id, - label: appMenuItem.controlProps.label, - description: appMenuItem.controlProps.description ?? appMenuItem.controlProps.label, - testId: appMenuItem.controlProps.testId, - run: async (anchorElement: HTMLElement) => { - await runAppMenuAction({ - appMenuItem, - anchorElement, - getDiscoverParams, - }); - }, - ...(appMenuItem.type === AppMenuActionType.primary - ? { iconType: appMenuItem.controlProps.iconType, iconOnly: true } - : {}), - }; -} From 53bacc165a0ec077950f89ecbd85b7be8fb5e4a1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 17:14:24 +0200 Subject: [PATCH 13/61] [Discover] Clean up unused customization option --- .../public/plugin.tsx | 108 +----------------- .../src/components/app_menu/types.ts | 27 +++-- .../top_nav/discover_topnav.test.tsx | 47 -------- .../top_nav/get_top_nav_badges.test.ts | 41 ------- .../components/top_nav/get_top_nav_links.tsx | 14 +-- .../top_nav_customization.ts | 15 --- 6 files changed, 21 insertions(+), 231 deletions(-) diff --git a/examples/discover_customization_examples/public/plugin.tsx b/examples/discover_customization_examples/public/plugin.tsx index 7c35287b843ba..6dc6e8f48da58 100644 --- a/examples/discover_customization_examples/public/plugin.tsx +++ b/examples/discover_customization_examples/public/plugin.tsx @@ -7,17 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - EuiButton, - EuiContextMenu, - EuiFlexItem, - EuiPopover, - EuiWrappingPopover, - IconType, -} from '@elastic/eui'; +import { EuiButton, EuiContextMenu, EuiFlexItem, EuiPopover, IconType } from '@elastic/eui'; import { CoreSetup, CoreStart, Plugin, SimpleSavedObject } from '@kbn/core/public'; import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { CustomizationCallback, DiscoverSetup, @@ -102,112 +94,14 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin { } start(core: CoreStart, plugins: DiscoverCustomizationExamplesStartPlugins) { - const { discover } = plugins; - - let isOptionsOpen = false; - const optionsContainer = document.createElement('div'); - const closeOptionsPopover = () => { - ReactDOM.unmountComponentAtNode(optionsContainer); - document.body.removeChild(optionsContainer); - isOptionsOpen = false; - }; - this.customizationCallback = ({ customizations, stateContainer }) => { customizations.set({ id: 'top_nav', defaultMenu: { newItem: { disabled: true }, openItem: { disabled: true }, - shareItem: { order: 200 }, alertsItem: { disabled: true }, inspectItem: { disabled: true }, - saveItem: { order: 400 }, - }, - getMenuItems: () => [ - { - data: { - id: 'options', - label: 'Options', - iconType: 'arrowDown', - iconSide: 'right', - testId: 'customOptionsButton', - run: (anchorElement: HTMLElement) => { - if (isOptionsOpen) { - closeOptionsPopover(); - return; - } - - isOptionsOpen = true; - document.body.appendChild(optionsContainer); - - const element = ( - - - alert('Create new clicked'), - }, - { - name: 'Make a copy', - icon: 'copy', - onClick: () => alert('Make a copy clicked'), - }, - { - name: 'Manage saved searches', - icon: 'gear', - onClick: () => alert('Manage saved searches clicked'), - }, - ], - }, - ]} - data-test-subj="customOptionsPopover" - /> - - - ); - - ReactDOM.render(element, optionsContainer); - }, - }, - order: 100, - }, - { - data: { - id: 'documentExplorer', - label: 'Document explorer', - iconType: 'discoverApp', - testId: 'documentExplorerButton', - run: () => { - discover.locator?.navigate({}); - }, - }, - order: 300, - }, - ], - getBadges: () => { - return [ - { - data: { - badgeText: 'Example badge', - color: 'warning', - }, - order: 10, - }, - ]; }, }); diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 8ad14d4bf3d79..ff2147724b5ad 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -43,32 +43,31 @@ export enum AppMenuActionType { } interface AppMenuActionBase { - id: AppMenuActionId | string; - order?: number; + readonly id: AppMenuActionId | string; + readonly order?: number; } export interface AppMenuPopoverAction extends AppMenuActionBase { - type: AppMenuActionType.secondary | AppMenuActionType.custom; - controlProps: AppMenuControlProps; + readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; + readonly controlProps: AppMenuControlProps; } export interface AppMenuAction extends AppMenuActionBase { - type: AppMenuActionType.secondary | AppMenuActionType.custom; - controlProps: AppMenuControlProps; + readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; + readonly controlProps: AppMenuControlProps; } export interface AppMenuIconAction extends AppMenuActionBase { - type: AppMenuActionType.primary; - controlProps: AppMenuIconControlProps; + readonly type: AppMenuActionType.primary; + readonly controlProps: AppMenuIconControlProps; } export interface AppMenuPopoverActions extends AppMenuActionBase { - label: TopNavMenuData['label']; - description?: TopNavMenuData['description']; - testId?: TopNavMenuData['testId']; - type: AppMenuActionType.secondary | AppMenuActionType.custom; - actions: AppMenuPopoverAction[]; + readonly label: TopNavMenuData['label']; + readonly description?: TopNavMenuData['description']; + readonly testId?: TopNavMenuData['testId']; + readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; + readonly actions: AppMenuPopoverAction[]; } export type AppMenuItem = AppMenuPopoverActions | AppMenuAction | AppMenuIconAction; -export type AppMenuItems = AppMenuItem[]; diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx index d99cbd91ffd35..fcaf50effb8c1 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx @@ -83,7 +83,6 @@ const mockUseKibana = useKibana as jest.Mock; describe('Discover topnav component', () => { beforeEach(() => { mockTopNavCustomization.defaultMenu = undefined; - mockTopNavCustomization.getMenuItems = undefined; mockUseCustomizations = false; jest.clearAllMocks(); @@ -158,31 +157,6 @@ describe('Discover topnav component', () => { }); describe('top nav customization', () => { - it('should call getMenuItems', () => { - mockUseCustomizations = true; - mockTopNavCustomization.getMenuItems = jest.fn(() => [ - { - data: { - id: 'test', - label: 'Test', - testId: 'testButton', - run: () => {}, - }, - order: 350, - }, - ]); - const props = getProps(); - const component = mountWithIntl( - - - - ); - expect(mockTopNavCustomization.getMenuItems).toHaveBeenCalledTimes(1); - const topNavMenu = component.find(TopNavMenu); - const topMenuConfig = topNavMenu.props().config?.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['inspect', 'new', 'test', 'open', 'share', 'save']); - }); - it('should allow disabling default menu items', () => { mockUseCustomizations = true; mockTopNavCustomization.defaultMenu = { @@ -203,27 +177,6 @@ describe('Discover topnav component', () => { const topMenuConfig = topNavMenu.props().config?.map((obj: TopNavMenuData) => obj.id); expect(topMenuConfig).toEqual([]); }); - - it('should allow reordering default menu items', () => { - mockUseCustomizations = true; - mockTopNavCustomization.defaultMenu = { - newItem: { order: 6 }, - openItem: { order: 5 }, - shareItem: { order: 4 }, - alertsItem: { order: 3 }, - inspectItem: { order: 2 }, - saveItem: { order: 1 }, - }; - const props = getProps(); - const component = mountWithIntl( - - - - ); - const topNavMenu = component.find(TopNavMenu); - const topMenuConfig = topNavMenu.props().config?.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['save', 'inspect', 'share', 'open', 'new']); - }); }); describe('search bar customization', () => { diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.test.ts index 89454c79ab22c..d6374affe0f61 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.test.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.test.ts @@ -128,45 +128,4 @@ describe('getTopNavBadges()', function () { }); expect(topNavBadges).toMatchInlineSnapshot(`Array []`); }); - - test('should allow to render additional badges when customized', () => { - const topNavBadges = getTopNavBadges({ - hasUnsavedChanges: true, - services: discoverServiceMock, - stateContainer, - topNavCustomization: { - id: 'top_nav', - getBadges: () => { - return [ - { - data: { - badgeText: 'test10', - }, - order: 10, - }, - { - data: { - badgeText: 'test200', - }, - order: 200, - }, - ]; - }, - }, - }); - expect(topNavBadges).toMatchInlineSnapshot(` - Array [ - Object { - "badgeText": "test10", - }, - Object { - "badgeText": "Unsaved changes", - "renderCustomBadge": [Function], - }, - Object { - "badgeText": "test200", - }, - ] - `); - }); }); diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 9c5c223dcd7df..870b0fb550265 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -171,14 +171,14 @@ export const getTopNavLinks = ({ // TODO: allow to extend with custom items const defaultMenu = topNavCustomization?.defaultMenu; - const entries = [...(topNavCustomization?.getMenuItems?.() ?? [])]; + const entries = []; if (services.uiSettings.get(ENABLE_ESQL)) { entries.push({ data: esqLDataViewTransitionToggle, order: 0 }); } if (!defaultMenu?.inspectItem?.disabled) { - entries.push({ data: inspectSearch, order: defaultMenu?.inspectItem?.order ?? 100 }); + entries.push({ data: inspectSearch, order: 100 }); } if ( @@ -186,23 +186,23 @@ export const getTopNavLinks = ({ services.capabilities.management?.insightsAndAlerting?.triggersActions && !defaultMenu?.alertsItem?.disabled ) { - entries.push({ data: alerts, order: defaultMenu?.alertsItem?.order ?? 200 }); + entries.push({ data: alerts, order: 200 }); } if (!defaultMenu?.newItem?.disabled) { - entries.push({ data: newSearch, order: defaultMenu?.newItem?.order ?? 300 }); + entries.push({ data: newSearch, order: 300 }); } if (!defaultMenu?.openItem?.disabled) { - entries.push({ data: openSearch, order: defaultMenu?.openItem?.order ?? 400 }); + entries.push({ data: openSearch, order: 400 }); } if (!defaultMenu?.shareItem?.disabled) { - entries.push({ data: shareSearch, order: defaultMenu?.shareItem?.order ?? 500 }); + entries.push({ data: shareSearch, order: 500 }); } if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) { - entries.push({ data: saveSearch, order: defaultMenu?.saveItem?.order ?? 600 }); + entries.push({ data: saveSearch, order: 600 }); } return entries.sort((a, b) => a.order - b.order).map((entry) => entry.data); diff --git a/src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts b/src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts index 865c52f211aff..1bbc6adee520f 100644 --- a/src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts +++ b/src/plugins/discover/public/customizations/customization_types/top_nav_customization.ts @@ -7,11 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { TopNavMenuData, TopNavMenuBadgeProps } from '@kbn/navigation-plugin/public'; - export interface TopNavDefaultItem { disabled?: boolean; - order?: number; } export interface TopNavDefaultMenu { @@ -23,24 +20,12 @@ export interface TopNavDefaultMenu { saveItem?: TopNavDefaultItem; } -export interface TopNavMenuItem { - data: TopNavMenuData; - order: number; -} - export interface TopNavDefaultBadges { unsavedChangesBadge?: TopNavDefaultItem; } -export interface TopNavBadge { - data: TopNavMenuBadgeProps; - order: number; -} - export interface TopNavCustomization { id: 'top_nav'; defaultMenu?: TopNavDefaultMenu; - getMenuItems?: () => TopNavMenuItem[]; defaultBadges?: TopNavDefaultBadges; - getBadges?: () => TopNavBadge[]; } From 4666a4dbdf4d0dc0a98a99e9c501316fd5bff2b8 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:27:05 +0000 Subject: [PATCH 14/61] [CI] Auto-commit changed files from 'node scripts/notice' --- examples/discover_customization_examples/tsconfig.json | 1 - packages/kbn-discover-utils/tsconfig.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/examples/discover_customization_examples/tsconfig.json b/examples/discover_customization_examples/tsconfig.json index 776153f943fac..30ff666575f1d 100644 --- a/examples/discover_customization_examples/tsconfig.json +++ b/examples/discover_customization_examples/tsconfig.json @@ -13,7 +13,6 @@ "@kbn/i18n-react", "@kbn/react-kibana-context-theme", "@kbn/data-plugin", - "@kbn/react-kibana-context-render", ], "exclude": ["target/**/*"] } diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index dd20548a66574..8f0218be3db44 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -29,10 +29,5 @@ "@kbn/expressions-plugin", "@kbn/logs-data-access-plugin", "@kbn/navigation-plugin", - "@kbn/core-application-browser", - "@kbn/triggers-actions-ui-plugin", - "@kbn/core-lifecycle-browser", - "@kbn/data-plugin", - "@kbn/core-capabilities-common" ] } From 0b6c15961fca52597ab816f1914b2b1193d985e8 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 9 Oct 2024 18:06:42 +0200 Subject: [PATCH 15/61] [Discover] Add a registry --- packages/kbn-discover-utils/index.ts | 1 + .../src/components/app_menu/types.ts | 7 +- packages/kbn-discover-utils/src/index.ts | 1 + .../components/top_nav/get_top_nav_links.tsx | 219 ++++++++---------- 4 files changed, 104 insertions(+), 124 deletions(-) diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index ed6d58ca3da8d..c291afa86ad0a 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -57,6 +57,7 @@ export { getVisibleColumns, canPrependTimeFieldColumn, DiscoverFlyouts, + AppMenuRegistry, dismissAllFlyoutsExceptFor, dismissFlyouts, } from './src'; diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index ff2147724b5ad..c0e4dd482ad20 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -47,11 +47,6 @@ interface AppMenuActionBase { readonly order?: number; } -export interface AppMenuPopoverAction extends AppMenuActionBase { - readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; - readonly controlProps: AppMenuControlProps; -} - export interface AppMenuAction extends AppMenuActionBase { readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; readonly controlProps: AppMenuControlProps; @@ -67,7 +62,7 @@ export interface AppMenuPopoverActions extends AppMenuActionBase { readonly description?: TopNavMenuData['description']; readonly testId?: TopNavMenuData['testId']; readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; - readonly actions: AppMenuPopoverAction[]; + readonly actions: AppMenuAction[]; } export type AppMenuItem = AppMenuPopoverActions | AppMenuAction | AppMenuIconAction; diff --git a/packages/kbn-discover-utils/src/index.ts b/packages/kbn-discover-utils/src/index.ts index 8fe9a9418c9fe..243dd05774448 100644 --- a/packages/kbn-discover-utils/src/index.ts +++ b/packages/kbn-discover-utils/src/index.ts @@ -14,3 +14,4 @@ export * from './utils'; export * from './data_types'; export * from './components/custom_control_columns'; +export { AppMenuRegistry } from './components/app_menu/app_menu_registry'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 870b0fb550265..216d7c574c4af 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -12,6 +12,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; +import { AppMenuItem, AppMenuRegistry } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; @@ -49,77 +50,6 @@ export const getTopNavLinks = ({ topNavCustomization: TopNavCustomization | undefined; shouldShowESQLToDataViewTransitionModal: boolean; }): TopNavMenuData[] => { - /** - * Switches from ES|QL to classic mode and vice versa - */ - const esqLDataViewTransitionToggle = { - id: 'esql', - label: isEsqlMode - ? i18n.translate('discover.localMenu.switchToClassicTitle', { - defaultMessage: 'Switch to classic', - }) - : i18n.translate('discover.localMenu.tryESQLTitle', { - defaultMessage: 'Try ES|QL', - }), - emphasize: true, - fill: false, - color: 'text', - tooltip: isEsqlMode - ? i18n.translate('discover.localMenu.switchToClassicTooltipLabel', { - defaultMessage: 'Switch to KQL or Lucene syntax.', - }) - : i18n.translate('discover.localMenu.esqlTooltipLabel', { - defaultMessage: `ES|QL is Elastic's powerful new piped query language.`, - }), - run: () => { - if (dataView) { - if (isEsqlMode) { - services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:back_to_classic_clicked`); - /** - * Display the transition modal if: - * - the user has not dismissed the modal - * - the user has opened and applied changes to the saved search - */ - if ( - shouldShowESQLToDataViewTransitionModal && - !services.storage.get(ESQL_TRANSITION_MODAL_KEY) - ) { - state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true); - } else { - state.actions.transitionFromESQLToDataView(dataView.id ?? ''); - } - } else { - state.actions.transitionFromDataViewToESQL(dataView); - services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:try_btn_clicked`); - } - } - }, - testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', - }; - - const saveSearch = { - id: 'save', - label: i18n.translate('discover.localMenu.saveTitle', { - defaultMessage: 'Save', - }), - description: i18n.translate('discover.localMenu.saveSearchDescription', { - defaultMessage: 'Save Search', - }), - testId: 'discoverSaveButton', - iconType: 'save', - emphasize: true, - run: (anchorElement: HTMLElement) => { - onSaveSearch({ - savedSearch: state.savedSearchState.getState(), - services, - state, - onClose: () => { - anchorElement?.focus(); - }, - }); - }, - }; - const getDiscoverParams = (): AppMenuDiscoverParams => ({ dataView, adHocDataViews, @@ -135,50 +65,12 @@ export const getTopNavLinks = ({ onOpenSavedSearch: state.actions.onOpenSavedSearch, }); - /* Primary items */ - - const newSearch = convertAppMenuItemToTopNavItem({ - appMenuItem: getNewSearchAppMenuItem({ - getDiscoverParams, - }), - services, - }); - - const openSearch = convertAppMenuItemToTopNavItem({ - appMenuItem: getOpenSearchAppMenuItem({ getDiscoverParams }), - services, - }); - - const shareSearch = convertAppMenuItemToTopNavItem({ - appMenuItem: getShareAppMenuItem({ getDiscoverParams, stateContainer: state }), - services, - }); - - /* Secondary items */ - - const alerts = convertAppMenuItemToTopNavItem({ - appMenuItem: getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }), - services, - }); - // TODO: allow to extend the alerts menu - - const inspectSearch = convertAppMenuItemToTopNavItem({ - appMenuItem: getInspectAppMenuItem({ onOpenInspector }), - services, - }); - - /* Custom items */ - // TODO: allow to extend with custom items - const defaultMenu = topNavCustomization?.defaultMenu; - const entries = []; - - if (services.uiSettings.get(ENABLE_ESQL)) { - entries.push({ data: esqLDataViewTransitionToggle, order: 0 }); - } + const appMenuPrimaryAndSecondaryItems: AppMenuItem[] = []; if (!defaultMenu?.inspectItem?.disabled) { - entries.push({ data: inspectSearch, order: 100 }); + const inspectAppMenuItem = getInspectAppMenuItem({ onOpenInspector }); + appMenuPrimaryAndSecondaryItems.push(inspectAppMenuItem); } if ( @@ -186,24 +78,115 @@ export const getTopNavLinks = ({ services.capabilities.management?.insightsAndAlerting?.triggersActions && !defaultMenu?.alertsItem?.disabled ) { - entries.push({ data: alerts, order: 200 }); + const alertsAppMenuItem = getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }); + appMenuPrimaryAndSecondaryItems.push(alertsAppMenuItem); } if (!defaultMenu?.newItem?.disabled) { - entries.push({ data: newSearch, order: 300 }); + const newSearchMenuItem = getNewSearchAppMenuItem({ + getDiscoverParams, + }); + appMenuPrimaryAndSecondaryItems.push(newSearchMenuItem); } if (!defaultMenu?.openItem?.disabled) { - entries.push({ data: openSearch, order: 400 }); + const openSearchMenuItem = getOpenSearchAppMenuItem({ getDiscoverParams }); + appMenuPrimaryAndSecondaryItems.push(openSearchMenuItem); } if (!defaultMenu?.shareItem?.disabled) { - entries.push({ data: shareSearch, order: 500 }); + const shareAppMenuItem = getShareAppMenuItem({ getDiscoverParams, stateContainer: state }); + appMenuPrimaryAndSecondaryItems.push(shareAppMenuItem); + } + + const appMenuRegistry = new AppMenuRegistry(appMenuPrimaryAndSecondaryItems); + + /* Custom items */ + // TODO: allow to extend with custom items + + const entries = appMenuRegistry.getSortedItems().map((appMenuItem) => + convertAppMenuItemToTopNavItem({ + appMenuItem, + services, + }) + ); + + if (services.uiSettings.get(ENABLE_ESQL)) { + /** + * Switches from ES|QL to classic mode and vice versa + */ + const esqLDataViewTransitionToggle = { + id: 'esql', + label: isEsqlMode + ? i18n.translate('discover.localMenu.switchToClassicTitle', { + defaultMessage: 'Switch to classic', + }) + : i18n.translate('discover.localMenu.tryESQLTitle', { + defaultMessage: 'Try ES|QL', + }), + emphasize: true, + fill: false, + color: 'text', + tooltip: isEsqlMode + ? i18n.translate('discover.localMenu.switchToClassicTooltipLabel', { + defaultMessage: 'Switch to KQL or Lucene syntax.', + }) + : i18n.translate('discover.localMenu.esqlTooltipLabel', { + defaultMessage: `ES|QL is Elastic's powerful new piped query language.`, + }), + run: () => { + if (dataView) { + if (isEsqlMode) { + services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:back_to_classic_clicked`); + /** + * Display the transition modal if: + * - the user has not dismissed the modal + * - the user has opened and applied changes to the saved search + */ + if ( + shouldShowESQLToDataViewTransitionModal && + !services.storage.get(ESQL_TRANSITION_MODAL_KEY) + ) { + state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true); + } else { + state.actions.transitionFromESQLToDataView(dataView.id ?? ''); + } + } else { + state.actions.transitionFromDataViewToESQL(dataView); + services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:try_btn_clicked`); + } + } + }, + testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', + }; + entries.unshift(esqLDataViewTransitionToggle); } if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) { - entries.push({ data: saveSearch, order: 600 }); + const saveSearch = { + id: 'save', + label: i18n.translate('discover.localMenu.saveTitle', { + defaultMessage: 'Save', + }), + description: i18n.translate('discover.localMenu.saveSearchDescription', { + defaultMessage: 'Save Search', + }), + testId: 'discoverSaveButton', + iconType: 'save', + emphasize: true, + run: (anchorElement: HTMLElement) => { + onSaveSearch({ + savedSearch: state.savedSearchState.getState(), + services, + state, + onClose: () => { + anchorElement?.focus(); + }, + }); + }, + }; + entries.push(saveSearch); } - return entries.sort((a, b) => a.order - b.order).map((entry) => entry.data); + return entries; }; From df1b0dd2e34251e9d78f3ab43fca6a45445d58e5 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 10:00:33 +0200 Subject: [PATCH 16/61] [Discover] Commit the registry --- .../components/app_menu/app_menu_registry.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts new file mode 100644 index 0000000000000..30f09f9fe4c44 --- /dev/null +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -0,0 +1,98 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + AppMenuItem, + AppMenuAction, + AppMenuActionId, + AppMenuPopoverActions, + AppMenuActionType, +} from './types'; + +export class AppMenuRegistry { + private readonly appMenuItems: AppMenuItem[]; + + constructor(primaryAndSecondaryActions: AppMenuItem[]) { + this.appMenuItems = assignOrderToActions(primaryAndSecondaryActions); + } + + public registerCustomAction(appMenuItem: AppMenuAction) { + this.appMenuItems.push(appMenuItem); + } + + public registerCustomActionUnderAlerts(appMenuItem: AppMenuAction) { + const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); + if (alertsMenuItem && isAppMenuActionsPopover(alertsMenuItem)) { + // insert the custom action before the last item in the alerts menu + alertsMenuItem.actions.splice(alertsMenuItem.actions.length - 1, 0, appMenuItem); + } + } + + public getSortedItems() { + const primaryActions = sortAppMenuItems( + this.appMenuItems.filter((item) => item.type === AppMenuActionType.primary) + ); + const secondaryActions = sortAppMenuItems( + this.appMenuItems.filter((item) => item.type === AppMenuActionType.secondary) + ); + const customActions = sortAppMenuItems( + this.appMenuItems.filter((item) => item.type === AppMenuActionType.custom) + ); + + return [...customActions, ...secondaryActions, ...primaryActions]; + } +} + +function isAppMenuActionsPopover(appMenuItem: AppMenuItem): appMenuItem is AppMenuPopoverActions { + return 'actions' in appMenuItem; +} + +function sortByOrder(a: AppMenuItem, b: AppMenuItem): number { + return (a.order ?? 0) - (b.order ?? 0); +} + +function sortAppMenuItems(appMenuItems: AppMenuItem[]): AppMenuItem[] { + const sortedAppMenuItems = [...appMenuItems].sort(sortByOrder); + return sortedAppMenuItems.map((appMenuItem) => { + if (isAppMenuActionsPopover(appMenuItem)) { + const popoverWithSortedActions: AppMenuPopoverActions = { + ...appMenuItem, + actions: [...appMenuItem.actions].sort(sortByOrder), + }; + return popoverWithSortedActions; + } + return appMenuItem; + }); +} + +function assignOrderToActions(appMenuItems: AppMenuItem[]): AppMenuItem[] { + let order = 0; + return appMenuItems.map((appMenuItem) => { + order = order + 100; + if (isAppMenuActionsPopover(appMenuItem)) { + let orderInPopover = 0; + const actionsWithOrder = appMenuItem.actions.map((action) => { + orderInPopover = orderInPopover + 100; + return { + ...action, + order: action.order ?? orderInPopover, + }; + }); + return { + ...appMenuItem, + order: appMenuItem.order ?? order, + actions: actionsWithOrder, + }; + } + return { + ...appMenuItem, + order: appMenuItem.order ?? order, + }; + }); +} From 36a451487ebac6bdd90167e87710c56a5542588b Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 11:02:24 +0200 Subject: [PATCH 17/61] [Discover] Fix lint issues --- .../main/components/top_nav/get_top_nav_badges.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.tsx index ee8187433ff44..e786fd7298b7e 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_badges.tsx @@ -40,7 +40,7 @@ export const getTopNavBadges = ({ }); const defaultBadges = topNavCustomization?.defaultBadges; - const entries = [...(topNavCustomization?.getBadges?.() ?? [])]; + const entries = []; const isManaged = stateContainer.savedSearchState.getState().managed; @@ -63,7 +63,7 @@ export const getTopNavBadges = ({ } : undefined, }), - order: defaultBadges?.unsavedChangesBadge?.order ?? 100, + order: 100, }); } From 0817e7a2e4b418124f6c90f6d88508b9e6f49e8c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 13:44:40 +0200 Subject: [PATCH 18/61] [Discover] Introduce the new extension point --- .../app_menu_actions/get_new_search.tsx | 6 +- .../app_menu_actions/get_open_search.tsx | 6 +- .../top_nav/app_menu_actions/types.ts | 13 +- .../components/top_nav/get_top_nav_links.tsx | 192 --------------- .../components/top_nav/use_discover_topnav.ts | 35 +-- ...nks.test.ts => use_top_nav_links.test.tsx} | 61 +++-- .../components/top_nav/use_top_nav_links.tsx | 224 ++++++++++++++++++ .../public/context_awareness/types.ts | 37 ++- 8 files changed, 314 insertions(+), 260 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx rename src/plugins/discover/public/application/main/components/top_nav/{get_top_nav_links.test.ts => use_top_nav_links.test.tsx} (76%) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index d857e1f1f945c..ddc3c8ac32d04 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -9,12 +9,11 @@ import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; -import { AppMenuDiscoverParams } from './types'; export const getNewSearchAppMenuItem = ({ - getDiscoverParams, + onNewSearch, }: { - getDiscoverParams: () => AppMenuDiscoverParams; + onNewSearch: () => void; }): AppMenuIconAction => { return { id: AppMenuActionId.new, @@ -29,7 +28,6 @@ export const getNewSearchAppMenuItem = ({ iconType: 'plus', testId: 'discoverNewButton', onClick: () => { - const { onNewSearch } = getDiscoverParams(); onNewSearch(); }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index e986ab969e881..a7550a0941f33 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -11,12 +11,11 @@ import React from 'react'; import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; -import { AppMenuDiscoverParams } from './types'; export const getOpenSearchAppMenuItem = ({ - getDiscoverParams, + onOpenSavedSearch, }: { - getDiscoverParams: () => AppMenuDiscoverParams; + onOpenSavedSearch: (savedSearchId: string) => void; }): AppMenuIconAction => { return { id: AppMenuActionId.open, @@ -31,7 +30,6 @@ export const getOpenSearchAppMenuItem = ({ iconType: 'folderOpen', testId: 'discoverOpenButton', onClick: ({ onFinishAction }) => { - const { onOpenSavedSearch } = getDiscoverParams(); return ; }, }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts index cc5f98a916b78..9f4fe8238c183 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/types.ts @@ -7,15 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { DiscoverServices } from '../../../../../build_services'; +import type { AppMenuExtensionParams } from '../../../../../context_awareness/types'; -export interface AppMenuDiscoverParams { - dataView: DataView | undefined; - adHocDataViews: DataView[]; - isEsqlMode?: boolean; - services: DiscoverServices; - onNewSearch: () => void; - onOpenSavedSearch: (id: string) => void; - onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; -} +export type AppMenuDiscoverParams = AppMenuExtensionParams; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx deleted file mode 100644 index 216d7c574c4af..0000000000000 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { AppMenuItem, AppMenuRegistry } from '@kbn/discover-utils'; -import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; -import { DiscoverServices } from '../../../../build_services'; -import { onSaveSearch } from './on_save_search'; -import { DiscoverStateContainer } from '../../state_management/discover_state'; -import { - getAlertsAppMenuItem, - getNewSearchAppMenuItem, - getOpenSearchAppMenuItem, - getShareAppMenuItem, - getInspectAppMenuItem, - convertAppMenuItemToTopNavItem, - AppMenuDiscoverParams, -} from './app_menu_actions'; -import type { TopNavCustomization } from '../../../../customizations'; - -/** - * Helper function to build the top nav links - */ -export const getTopNavLinks = ({ - dataView, - services, - state, - onOpenInspector, - isEsqlMode, - adHocDataViews, - topNavCustomization, - shouldShowESQLToDataViewTransitionModal, -}: { - dataView: DataView | undefined; - services: DiscoverServices; - state: DiscoverStateContainer; - onOpenInspector: () => void; - isEsqlMode: boolean; - adHocDataViews: DataView[]; - topNavCustomization: TopNavCustomization | undefined; - shouldShowESQLToDataViewTransitionModal: boolean; -}): TopNavMenuData[] => { - const getDiscoverParams = (): AppMenuDiscoverParams => ({ - dataView, - adHocDataViews, - isEsqlMode, - services, - onUpdateAdHocDataViews: async (adHocDataViewList) => { - await state.actions.loadDataViewList(); - state.internalState.transitions.setAdHocDataViews(adHocDataViewList); - }, - onNewSearch: () => { - services.locator.navigate({}); - }, - onOpenSavedSearch: state.actions.onOpenSavedSearch, - }); - - const defaultMenu = topNavCustomization?.defaultMenu; - const appMenuPrimaryAndSecondaryItems: AppMenuItem[] = []; - - if (!defaultMenu?.inspectItem?.disabled) { - const inspectAppMenuItem = getInspectAppMenuItem({ onOpenInspector }); - appMenuPrimaryAndSecondaryItems.push(inspectAppMenuItem); - } - - if ( - services.triggersActionsUi && - services.capabilities.management?.insightsAndAlerting?.triggersActions && - !defaultMenu?.alertsItem?.disabled - ) { - const alertsAppMenuItem = getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }); - appMenuPrimaryAndSecondaryItems.push(alertsAppMenuItem); - } - - if (!defaultMenu?.newItem?.disabled) { - const newSearchMenuItem = getNewSearchAppMenuItem({ - getDiscoverParams, - }); - appMenuPrimaryAndSecondaryItems.push(newSearchMenuItem); - } - - if (!defaultMenu?.openItem?.disabled) { - const openSearchMenuItem = getOpenSearchAppMenuItem({ getDiscoverParams }); - appMenuPrimaryAndSecondaryItems.push(openSearchMenuItem); - } - - if (!defaultMenu?.shareItem?.disabled) { - const shareAppMenuItem = getShareAppMenuItem({ getDiscoverParams, stateContainer: state }); - appMenuPrimaryAndSecondaryItems.push(shareAppMenuItem); - } - - const appMenuRegistry = new AppMenuRegistry(appMenuPrimaryAndSecondaryItems); - - /* Custom items */ - // TODO: allow to extend with custom items - - const entries = appMenuRegistry.getSortedItems().map((appMenuItem) => - convertAppMenuItemToTopNavItem({ - appMenuItem, - services, - }) - ); - - if (services.uiSettings.get(ENABLE_ESQL)) { - /** - * Switches from ES|QL to classic mode and vice versa - */ - const esqLDataViewTransitionToggle = { - id: 'esql', - label: isEsqlMode - ? i18n.translate('discover.localMenu.switchToClassicTitle', { - defaultMessage: 'Switch to classic', - }) - : i18n.translate('discover.localMenu.tryESQLTitle', { - defaultMessage: 'Try ES|QL', - }), - emphasize: true, - fill: false, - color: 'text', - tooltip: isEsqlMode - ? i18n.translate('discover.localMenu.switchToClassicTooltipLabel', { - defaultMessage: 'Switch to KQL or Lucene syntax.', - }) - : i18n.translate('discover.localMenu.esqlTooltipLabel', { - defaultMessage: `ES|QL is Elastic's powerful new piped query language.`, - }), - run: () => { - if (dataView) { - if (isEsqlMode) { - services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:back_to_classic_clicked`); - /** - * Display the transition modal if: - * - the user has not dismissed the modal - * - the user has opened and applied changes to the saved search - */ - if ( - shouldShowESQLToDataViewTransitionModal && - !services.storage.get(ESQL_TRANSITION_MODAL_KEY) - ) { - state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true); - } else { - state.actions.transitionFromESQLToDataView(dataView.id ?? ''); - } - } else { - state.actions.transitionFromDataViewToESQL(dataView); - services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:try_btn_clicked`); - } - } - }, - testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', - }; - entries.unshift(esqLDataViewTransitionToggle); - } - - if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) { - const saveSearch = { - id: 'save', - label: i18n.translate('discover.localMenu.saveTitle', { - defaultMessage: 'Save', - }), - description: i18n.translate('discover.localMenu.saveSearchDescription', { - defaultMessage: 'Save Search', - }), - testId: 'discoverSaveButton', - iconType: 'save', - emphasize: true, - run: (anchorElement: HTMLElement) => { - onSaveSearch({ - savedSearch: state.savedSearchState.getState(), - services, - state, - onClose: () => { - anchorElement?.focus(); - }, - }); - }, - }; - entries.push(saveSearch); - } - - return entries; -}; diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts index b36b1c4609ddf..b9ab31be6a684 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts @@ -20,7 +20,7 @@ import { } from '../../state_management/discover_state_provider'; import type { DiscoverStateContainer } from '../../state_management/discover_state'; import { getTopNavBadges } from './get_top_nav_badges'; -import { getTopNavLinks } from './get_top_nav_links'; +import { useTopNavLinks } from './use_top_nav_links'; export const useDiscoverTopNav = ({ stateContainer, @@ -55,29 +55,16 @@ export const useDiscoverTopNav = ({ stateContainer, }); - const topNavMenu = useMemo( - () => - getTopNavLinks({ - dataView, - services, - state: stateContainer, - onOpenInspector, - isEsqlMode, - adHocDataViews, - topNavCustomization, - shouldShowESQLToDataViewTransitionModal, - }), - [ - adHocDataViews, - dataView, - isEsqlMode, - onOpenInspector, - services, - stateContainer, - topNavCustomization, - shouldShowESQLToDataViewTransitionModal, - ] - ); + const topNavMenu = useTopNavLinks({ + dataView, + services, + state: stateContainer, + onOpenInspector, + isEsqlMode, + adHocDataViews, + topNavCustomization, + shouldShowESQLToDataViewTransitionModal, + }); return { topNavMenu, diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx similarity index 76% rename from src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts rename to src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx index 48f10944fc1ed..3d7c315247e47 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx @@ -7,13 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getTopNavLinks } from './get_top_nav_links'; +import React from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { useTopNavLinks } from './use_top_nav_links'; import { DiscoverServices } from '../../../../build_services'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; -describe('getTopNavLinks', () => { +describe('useTopNavLinks', () => { const services = { ...createDiscoverServicesMock(), capabilities: { @@ -29,17 +32,24 @@ describe('getTopNavLinks', () => { const state = getDiscoverStateMock({ isTimeBased: true }); state.actions.setDataView(dataViewMock); - test('getTopNavLinks result', () => { - const topNavLinks = getTopNavLinks({ - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: false, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, - }); + const Wrapper = ({ children }: { children: React.ReactNode }) => { + return {children}; + }; + + test('useTopNavLinks result', () => { + const topNavLinks = renderHook(useTopNavLinks, { + initialProps: { + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: false, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }, + wrapper: Wrapper, + }).result.current; expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { @@ -99,17 +109,20 @@ describe('getTopNavLinks', () => { `); }); - test('getTopNavLinks result for ES|QL mode', () => { - const topNavLinks = getTopNavLinks({ - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: true, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, - }); + test('useTopNavLinks result for ES|QL mode', () => { + const topNavLinks = renderHook(useTopNavLinks, { + initialProps: { + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: true, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }, + wrapper: Wrapper, + }).result.current; expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx new file mode 100644 index 0000000000000..e3ed17184a68e --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx @@ -0,0 +1,224 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { ENABLE_ESQL } from '@kbn/esql-utils'; +import { AppMenuItem, AppMenuRegistry } from '@kbn/discover-utils'; +import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; +import { DiscoverServices } from '../../../../build_services'; +import { onSaveSearch } from './on_save_search'; +import { DiscoverStateContainer } from '../../state_management/discover_state'; +import { + getAlertsAppMenuItem, + getNewSearchAppMenuItem, + getOpenSearchAppMenuItem, + getShareAppMenuItem, + getInspectAppMenuItem, + convertAppMenuItemToTopNavItem, + AppMenuDiscoverParams, +} from './app_menu_actions'; +import type { TopNavCustomization } from '../../../../customizations'; +import { useProfileAccessor } from '../../../../context_awareness'; + +/** + * Helper function to build the top nav links + */ +export const useTopNavLinks = ({ + dataView, + services, + state, + onOpenInspector, + isEsqlMode, + adHocDataViews, + topNavCustomization, + shouldShowESQLToDataViewTransitionModal, +}: { + dataView: DataView | undefined; + services: DiscoverServices; + state: DiscoverStateContainer; + onOpenInspector: () => void; + isEsqlMode: boolean; + adHocDataViews: DataView[]; + topNavCustomization: TopNavCustomization | undefined; + shouldShowESQLToDataViewTransitionModal: boolean; +}): TopNavMenuData[] => { + const getDiscoverParams = useCallback( + (): AppMenuDiscoverParams => ({ + isEsqlMode, + services, + dataView, + adHocDataViews, + onUpdateAdHocDataViews: async (adHocDataViewList) => { + await state.actions.loadDataViewList(); + state.internalState.transitions.setAdHocDataViews(adHocDataViewList); + }, + }), + [isEsqlMode, services, dataView, adHocDataViews, state] + ); + + const defaultMenu = topNavCustomization?.defaultMenu; + + const appMenuPrimaryAndSecondaryItems: AppMenuItem[] = useMemo(() => { + const items: AppMenuItem[] = []; + if (!defaultMenu?.inspectItem?.disabled) { + const inspectAppMenuItem = getInspectAppMenuItem({ onOpenInspector }); + items.push(inspectAppMenuItem); + } + + if ( + services.triggersActionsUi && + services.capabilities.management?.insightsAndAlerting?.triggersActions && + !defaultMenu?.alertsItem?.disabled + ) { + const alertsAppMenuItem = getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }); + items.push(alertsAppMenuItem); + } + + if (!defaultMenu?.newItem?.disabled) { + const newSearchMenuItem = getNewSearchAppMenuItem({ + onNewSearch: () => { + services.locator.navigate({}); + }, + }); + items.push(newSearchMenuItem); + } + + if (!defaultMenu?.openItem?.disabled) { + const openSearchMenuItem = getOpenSearchAppMenuItem({ + onOpenSavedSearch: state.actions.onOpenSavedSearch, + }); + items.push(openSearchMenuItem); + } + + if (!defaultMenu?.shareItem?.disabled) { + const shareAppMenuItem = getShareAppMenuItem({ getDiscoverParams, stateContainer: state }); + items.push(shareAppMenuItem); + } + + return items; + }, [getDiscoverParams, state, services, defaultMenu, onOpenInspector]); + + const appMenuRegistry = useMemo( + () => new AppMenuRegistry(appMenuPrimaryAndSecondaryItems), + [appMenuPrimaryAndSecondaryItems] + ); + + const getAppMenuAccessor = useProfileAccessor('getAppMenu'); + const appMenu = useMemo(() => { + const getAppMenu = getAppMenuAccessor(() => ({ + appMenuRegistry: (registry: AppMenuRegistry) => registry, + })); + + return getAppMenu(getDiscoverParams()); + }, [getAppMenuAccessor, getDiscoverParams]); + + return useMemo(() => { + const entries = appMenu + .appMenuRegistry(appMenuRegistry) + .getSortedItems() + .map((appMenuItem) => + convertAppMenuItemToTopNavItem({ + appMenuItem, + services, + }) + ); + + if (services.uiSettings.get(ENABLE_ESQL)) { + /** + * Switches from ES|QL to classic mode and vice versa + */ + const esqLDataViewTransitionToggle = { + id: 'esql', + label: isEsqlMode + ? i18n.translate('discover.localMenu.switchToClassicTitle', { + defaultMessage: 'Switch to classic', + }) + : i18n.translate('discover.localMenu.tryESQLTitle', { + defaultMessage: 'Try ES|QL', + }), + emphasize: true, + fill: false, + color: 'text', + tooltip: isEsqlMode + ? i18n.translate('discover.localMenu.switchToClassicTooltipLabel', { + defaultMessage: 'Switch to KQL or Lucene syntax.', + }) + : i18n.translate('discover.localMenu.esqlTooltipLabel', { + defaultMessage: `ES|QL is Elastic's powerful new piped query language.`, + }), + run: () => { + if (dataView) { + if (isEsqlMode) { + services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:back_to_classic_clicked`); + /** + * Display the transition modal if: + * - the user has not dismissed the modal + * - the user has opened and applied changes to the saved search + */ + if ( + shouldShowESQLToDataViewTransitionModal && + !services.storage.get(ESQL_TRANSITION_MODAL_KEY) + ) { + state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true); + } else { + state.actions.transitionFromESQLToDataView(dataView.id ?? ''); + } + } else { + state.actions.transitionFromDataViewToESQL(dataView); + services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:try_btn_clicked`); + } + } + }, + testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn', + }; + entries.unshift(esqLDataViewTransitionToggle); + } + + if (services.capabilities.discover.save && !defaultMenu?.saveItem?.disabled) { + const saveSearch = { + id: 'save', + label: i18n.translate('discover.localMenu.saveTitle', { + defaultMessage: 'Save', + }), + description: i18n.translate('discover.localMenu.saveSearchDescription', { + defaultMessage: 'Save Search', + }), + testId: 'discoverSaveButton', + iconType: 'save', + emphasize: true, + run: (anchorElement: HTMLElement) => { + onSaveSearch({ + savedSearch: state.savedSearchState.getState(), + services, + state, + onClose: () => { + anchorElement?.focus(); + }, + }); + }, + }; + entries.push(saveSearch); + } + + return entries; + }, [ + services, + appMenu, + appMenuRegistry, + state, + dataView, + isEsqlMode, + shouldShowESQLToDataViewTransitionModal, + defaultMenu, + ]); +}; diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 63c23bbb3d4b1..d43555ca21288 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -14,7 +14,7 @@ import type { UnifiedDataTableProps, } from '@kbn/unified-data-table'; import type { DocViewsRegistry } from '@kbn/unified-doc-viewer'; -import type { DataTableRecord } from '@kbn/discover-utils'; +import type { AppMenuRegistry, DataTableRecord } from '@kbn/discover-utils'; import type { CellAction, CellActionExecutionContext, CellActionsData } from '@kbn/cell-actions'; import type { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; @@ -23,6 +23,30 @@ import type { Trigger } from '@kbn/ui-actions-plugin/public'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DiscoverDataSource } from '../../common/data_sources'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; +import type { DiscoverServices } from '../build_services'; + +/** + * Supports extending the Discover app menu + */ +export interface AppMenuExtension { + /** + * Supports extending the app menu with additional actions + * @param prevRegistry The app menu registry + * @returns The updated app menu registry + */ + appMenuRegistry: (prevRegistry: AppMenuRegistry) => AppMenuRegistry; +} + +/** + * Parameters passed to the app menu extension + */ +export interface AppMenuExtensionParams { + isEsqlMode: boolean; + services: DiscoverServices; + dataView: DataView | undefined; + adHocDataViews: DataView[]; + onUpdateAdHocDataViews: (adHocDataViews: DataView[]) => Promise; +} /** * Supports customizing the Discover document viewer flyout @@ -283,4 +307,15 @@ export interface Profile { * @returns The doc viewer extension */ getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension; + + /** + * App Menu (Top Nav actions) + */ + + /** + * Supports extending the app menu with additional actions + * @param params The doc viewer extension parameters + * @returns The doc viewer extension + */ + getAppMenu: (params: AppMenuExtensionParams) => AppMenuExtension; } From 4270bf992ccb71ffbfcc504ab6fab6455edb23b1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 13:49:15 +0200 Subject: [PATCH 19/61] [Discover] Update tests --- .../discover_customization_examples/customizations.ts | 6 ------ .../discover_customization_examples/customizations.ts | 6 ------ 2 files changed, 12 deletions(-) diff --git a/test/examples/discover_customization_examples/customizations.ts b/test/examples/discover_customization_examples/customizations.ts index f9e29611dc0cc..38e8e8ab2a6c5 100644 --- a/test/examples/discover_customization_examples/customizations.ts +++ b/test/examples/discover_customization_examples/customizations.ts @@ -48,15 +48,9 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); it('Top nav', async () => { - await testSubjects.existOrFail('customOptionsButton'); await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('documentExplorerButton'); await testSubjects.missingOrFail('discoverNewButton'); await testSubjects.missingOrFail('discoverOpenButton'); - await testSubjects.click('customOptionsButton'); - await testSubjects.existOrFail('customOptionsPopover'); - await testSubjects.click('customOptionsButton'); - await testSubjects.missingOrFail('customOptionsPopover'); }); it('Search bar', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts index 59a0349c79580..406f00a894c4b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts @@ -46,15 +46,9 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); it('Top nav', async () => { - await testSubjects.existOrFail('customOptionsButton'); await testSubjects.existOrFail('shareTopNavButton'); - await testSubjects.existOrFail('documentExplorerButton'); await testSubjects.missingOrFail('discoverNewButton'); await testSubjects.missingOrFail('discoverOpenButton'); - await testSubjects.click('customOptionsButton'); - await testSubjects.existOrFail('customOptionsPopover'); - await testSubjects.click('customOptionsButton'); - await testSubjects.missingOrFail('customOptionsPopover'); }); it('Search bar', async () => { From e8f5da4a821f782cb86a0bfe8cec9fac3a5ff5f6 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 15:05:05 +0200 Subject: [PATCH 20/61] [Discover] Fix checks --- .../top_nav/use_top_nav_links.test.tsx | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx index 3d7c315247e47..51dcf7d4af0af 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.test.tsx @@ -32,24 +32,27 @@ describe('useTopNavLinks', () => { const state = getDiscoverStateMock({ isTimeBased: true }); state.actions.setDataView(dataViewMock); - const Wrapper = ({ children }: { children: React.ReactNode }) => { + const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { return {children}; }; test('useTopNavLinks result', () => { - const topNavLinks = renderHook(useTopNavLinks, { - initialProps: { - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: false, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, - }, - wrapper: Wrapper, - }).result.current; + const topNavLinks = renderHook( + () => + useTopNavLinks({ + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: false, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }), + { + wrapper: Wrapper, + } + ).result.current; expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { @@ -110,19 +113,22 @@ describe('useTopNavLinks', () => { }); test('useTopNavLinks result for ES|QL mode', () => { - const topNavLinks = renderHook(useTopNavLinks, { - initialProps: { - dataView: dataViewMock, - onOpenInspector: jest.fn(), - services, - state, - isEsqlMode: true, - adHocDataViews: [], - topNavCustomization: undefined, - shouldShowESQLToDataViewTransitionModal: false, - }, - wrapper: Wrapper, - }).result.current; + const topNavLinks = renderHook( + () => + useTopNavLinks({ + dataView: dataViewMock, + onOpenInspector: jest.fn(), + services, + state, + isEsqlMode: true, + adHocDataViews: [], + topNavCustomization: undefined, + shouldShowESQLToDataViewTransitionModal: false, + }), + { + wrapper: Wrapper, + } + ).result.current; expect(topNavLinks).toMatchInlineSnapshot(` Array [ Object { From a5a5474eff1f007b56cf1de42e38488457a11e15 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 10 Oct 2024 15:28:11 +0200 Subject: [PATCH 21/61] [Discover] Add to the example profile --- .../components/app_menu/app_menu_registry.ts | 8 ++- .../example_data_source_profile/profile.tsx | 68 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index 30f09f9fe4c44..d726d1d534915 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -22,7 +22,7 @@ export class AppMenuRegistry { this.appMenuItems = assignOrderToActions(primaryAndSecondaryActions); } - public registerCustomAction(appMenuItem: AppMenuAction) { + public registerCustomAction(appMenuItem: AppMenuAction | AppMenuPopoverActions) { this.appMenuItems.push(appMenuItem); } @@ -30,7 +30,7 @@ export class AppMenuRegistry { const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); if (alertsMenuItem && isAppMenuActionsPopover(alertsMenuItem)) { // insert the custom action before the last item in the alerts menu - alertsMenuItem.actions.splice(alertsMenuItem.actions.length - 1, 0, appMenuItem); + alertsMenuItem.actions.push(appMenuItem); } } @@ -53,8 +53,10 @@ function isAppMenuActionsPopover(appMenuItem: AppMenuItem): appMenuItem is AppMe return 'actions' in appMenuItem; } +const FALLBACK_ORDER = Number.MAX_SAFE_INTEGER; + function sortByOrder(a: AppMenuItem, b: AppMenuItem): number { - return (a.order ?? 0) - (b.order ?? 0); + return (a.order ?? FALLBACK_ORDER) - (b.order ?? FALLBACK_ORDER); } function sortAppMenuItems(appMenuItems: AppMenuItem[]): AppMenuItem[] { diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx index c82cf1a893c8d..ef44b870737de 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiBadge } from '@elastic/eui'; -import { getFieldValue, RowControlColumn } from '@kbn/discover-utils'; +import { EuiBadge, EuiFlyout } from '@elastic/eui'; +import { AppMenuActionType, getFieldValue, RowControlColumn } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -73,6 +73,70 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi }, }; }, + getAppMenu: (prev) => (params) => { + const prevValue = prev(params); + return { + appMenuRegistry: (registry) => { + registry.registerCustomAction({ + id: 'example-custom-action1', + type: AppMenuActionType.custom, + label: 'Custom 1', + actions: [ + { + id: 'example-custom-action11', + type: AppMenuActionType.custom, + controlProps: { + label: 'Custom 11', + onClick: () => { + alert('Example custom action 11 clicked'); + }, + }, + }, + { + id: 'example-custom-action12', + type: AppMenuActionType.custom, + controlProps: { + label: 'Custom 12', + onClick: () => { + alert('Example custom action 12 clicked'); + }, + }, + }, + ], + }); + + registry.registerCustomAction({ + id: 'example-custom-action2', + type: AppMenuActionType.custom, + controlProps: { + label: 'Custom 2', + onClick: ({ onFinishAction }) => { + return ( + +
Example custom action 2 clicked
+
+ ); + }, + }, + }); + + registry.registerCustomActionUnderAlerts({ + id: 'example-custom-action3', + type: AppMenuActionType.custom, + order: 101, + controlProps: { + label: 'Custom 3', + onClick: ({ onFinishAction }) => { + alert('Example custom action 3 clicked'); + onFinishAction(); + }, + }, + }); + + return prevValue.appMenuRegistry(registry); + }, + }; + }, getRowAdditionalLeadingControls: (prev) => (params) => { const additionalControls = prev(params) || []; From 1c88df479db4f7839a71b5ecb51d23bf6ae03812 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 11 Oct 2024 13:25:31 +0200 Subject: [PATCH 22/61] [Discover] Update types --- .../components/app_menu/app_menu_registry.ts | 28 +++++++++++-------- .../src/components/app_menu/types.ts | 24 ++++++++++------ .../top_nav/app_menu_actions/get_alerts.tsx | 4 +-- .../app_menu_actions/get_new_search.tsx | 4 +-- .../app_menu_actions/get_open_search.tsx | 4 +-- .../top_nav/app_menu_actions/get_share.tsx | 4 +-- .../app_menu_actions/run_app_menu_action.tsx | 12 +++++--- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index d726d1d534915..75cbc8f1f65bf 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -10,8 +10,8 @@ import { AppMenuItem, AppMenuAction, + AppMenuActionSubmenu, AppMenuActionId, - AppMenuPopoverActions, AppMenuActionType, } from './types'; @@ -22,26 +22,25 @@ export class AppMenuRegistry { this.appMenuItems = assignOrderToActions(primaryAndSecondaryActions); } - public registerCustomAction(appMenuItem: AppMenuAction | AppMenuPopoverActions) { + public registerCustomAction(appMenuItem: AppMenuAction | AppMenuActionSubmenu) { this.appMenuItems.push(appMenuItem); } public registerCustomActionUnderAlerts(appMenuItem: AppMenuAction) { const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); - if (alertsMenuItem && isAppMenuActionsPopover(alertsMenuItem)) { - // insert the custom action before the last item in the alerts menu + if (alertsMenuItem && isAppMenuActionSubmenu(alertsMenuItem)) { alertsMenuItem.actions.push(appMenuItem); } } public getSortedItems() { - const primaryActions = sortAppMenuItems( + const primaryActions = sortAppMenuItemsByOrder( this.appMenuItems.filter((item) => item.type === AppMenuActionType.primary) ); - const secondaryActions = sortAppMenuItems( + const secondaryActions = sortAppMenuItemsByOrder( this.appMenuItems.filter((item) => item.type === AppMenuActionType.secondary) ); - const customActions = sortAppMenuItems( + const customActions = sortAppMenuItemsByOrder( this.appMenuItems.filter((item) => item.type === AppMenuActionType.custom) ); @@ -49,7 +48,7 @@ export class AppMenuRegistry { } } -function isAppMenuActionsPopover(appMenuItem: AppMenuItem): appMenuItem is AppMenuPopoverActions { +function isAppMenuActionSubmenu(appMenuItem: AppMenuItem): appMenuItem is AppMenuActionSubmenu { return 'actions' in appMenuItem; } @@ -59,11 +58,11 @@ function sortByOrder(a: AppMenuItem, b: AppMenuItem): number { return (a.order ?? FALLBACK_ORDER) - (b.order ?? FALLBACK_ORDER); } -function sortAppMenuItems(appMenuItems: AppMenuItem[]): AppMenuItem[] { +function sortAppMenuItemsByOrder(appMenuItems: AppMenuItem[]): AppMenuItem[] { const sortedAppMenuItems = [...appMenuItems].sort(sortByOrder); return sortedAppMenuItems.map((appMenuItem) => { - if (isAppMenuActionsPopover(appMenuItem)) { - const popoverWithSortedActions: AppMenuPopoverActions = { + if (isAppMenuActionSubmenu(appMenuItem)) { + const popoverWithSortedActions: AppMenuActionSubmenu = { ...appMenuItem, actions: [...appMenuItem.actions].sort(sortByOrder), }; @@ -73,11 +72,16 @@ function sortAppMenuItems(appMenuItems: AppMenuItem[]): AppMenuItem[] { }); } +/** + * All primary and secondary actions by default get order 100, 200, 300,... assigned to them. + * Same for actions under a submenu. + * @param appMenuItems + */ function assignOrderToActions(appMenuItems: AppMenuItem[]): AppMenuItem[] { let order = 0; return appMenuItems.map((appMenuItem) => { order = order + 100; - if (isAppMenuActionsPopover(appMenuItem)) { + if (isAppMenuActionSubmenu(appMenuItem)) { let orderInPopover = 0; const actionsWithOrder = appMenuItem.actions.map((action) => { orderInPopover = orderInPopover + 100; diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index c0e4dd482ad20..ff7b57e234741 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -20,13 +20,12 @@ export type AppMenuControlProps = Pick< 'testId' | 'isLoading' | 'label' | 'description' | 'disableButton' | 'href' | 'tooltip' > & { onClick: - | (( - params: AppMenuControlOnClickParams - ) => Promise | React.ReactNode | void) + | ((params: AppMenuControlOnClickParams) => Promise) + | ((params: AppMenuControlOnClickParams) => React.ReactNode | void) | undefined; }; -export type AppMenuIconControlProps = AppMenuControlProps & Pick; +export type AppMenuControlIconOnlyProps = AppMenuControlProps & Pick; export enum AppMenuActionId { new = 'new', @@ -47,17 +46,26 @@ interface AppMenuActionBase { readonly order?: number; } +/** + * A normal menu action + */ export interface AppMenuAction extends AppMenuActionBase { readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; readonly controlProps: AppMenuControlProps; } -export interface AppMenuIconAction extends AppMenuActionBase { +/** + * A menu action with icon only + */ +export interface AppMenuActionIconOnly extends AppMenuActionBase { readonly type: AppMenuActionType.primary; - readonly controlProps: AppMenuIconControlProps; + readonly controlProps: AppMenuControlIconOnlyProps; } -export interface AppMenuPopoverActions extends AppMenuActionBase { +/** + * A menu action which opens a submenu with more actions + */ +export interface AppMenuActionSubmenu extends AppMenuActionBase { readonly label: TopNavMenuData['label']; readonly description?: TopNavMenuData['description']; readonly testId?: TopNavMenuData['testId']; @@ -65,4 +73,4 @@ export interface AppMenuPopoverActions extends AppMenuActionBase { readonly actions: AppMenuAction[]; } -export type AppMenuItem = AppMenuPopoverActions | AppMenuAction | AppMenuIconAction; +export type AppMenuItem = AppMenuActionSubmenu | AppMenuAction | AppMenuActionIconOnly; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index 01e6727bfdbf6..7ac613fcadd75 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; -import { AppMenuActionId, AppMenuActionType, AppMenuPopoverActions } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionSubmenu } from '@kbn/discover-utils'; import { AlertConsumers, ES_QUERY_ID, @@ -100,7 +100,7 @@ export const getAlertsAppMenuItem = ({ }: { getDiscoverParams: () => AppMenuDiscoverParams; stateContainer: DiscoverStateContainer; -}): AppMenuPopoverActions => { +}): AppMenuActionSubmenu => { const { dataView, services, isEsqlMode } = getDiscoverParams(); const timeField = getTimeField(dataView); const hasTimeFieldName = !isEsqlMode ? Boolean(dataView?.timeFieldName) : Boolean(timeField); diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index ddc3c8ac32d04..741f7d65c06f7 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -7,14 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionIconOnly } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; export const getNewSearchAppMenuItem = ({ onNewSearch, }: { onNewSearch: () => void; -}): AppMenuIconAction => { +}): AppMenuActionIconOnly => { return { id: AppMenuActionId.new, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index a7550a0941f33..bb7791f578831 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { AppMenuActionId, AppMenuActionType, AppMenuIconAction } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionIconOnly } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; @@ -16,7 +16,7 @@ export const getOpenSearchAppMenuItem = ({ onOpenSavedSearch, }: { onOpenSavedSearch: (savedSearchId: string) => void; -}): AppMenuIconAction => { +}): AppMenuActionIconOnly => { return { id: AppMenuActionId.open, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index 616dc91302ac4..faa467edb9e3c 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuIconAction, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; +import { AppMenuActionIconOnly, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; import { omit } from 'lodash'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { i18n } from '@kbn/i18n'; @@ -22,7 +22,7 @@ export const getShareAppMenuItem = ({ }: { stateContainer: DiscoverStateContainer; getDiscoverParams: () => AppMenuDiscoverParams; -}): AppMenuIconAction => { +}): AppMenuActionIconOnly => { return { id: AppMenuActionId.share, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx index 5c52aa688b688..f9cfefb5cc6b2 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx @@ -21,14 +21,18 @@ import ReactDOM from 'react-dom'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import type { AppMenuAction, AppMenuIconAction, AppMenuPopoverActions } from '@kbn/discover-utils'; +import type { + AppMenuAction, + AppMenuActionIconOnly, + AppMenuActionSubmenu, +} from '@kbn/discover-utils'; import type { DiscoverServices } from '../../../../../build_services'; const container = document.createElement('div'); let isOpen = false; interface AppMenuActionsMenuPopoverProps { - appMenuItem: AppMenuPopoverActions; + appMenuItem: AppMenuActionSubmenu; anchorElement: HTMLElement; services: DiscoverServices; onClose: () => void; @@ -110,7 +114,7 @@ export function runAppMenuPopoverAction({ anchorElement, services, }: { - appMenuItem: AppMenuPopoverActions; + appMenuItem: AppMenuActionSubmenu; anchorElement: HTMLElement; services: DiscoverServices; }) { @@ -142,7 +146,7 @@ export async function runAppMenuAction({ anchorElement, services, }: { - appMenuItem: AppMenuAction | AppMenuIconAction; + appMenuItem: AppMenuAction | AppMenuActionIconOnly; anchorElement: HTMLElement; services: DiscoverServices; }) { From 372004328c2e3dcb8ba2dd2237f9c437d61cab88 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 11 Oct 2024 14:53:39 +0200 Subject: [PATCH 23/61] [Discover] Make types stricter --- .../components/app_menu/app_menu_registry.ts | 79 ++++++++++++------- .../src/components/app_menu/types.ts | 57 ++++++++++--- .../top_nav/app_menu_actions/get_alerts.tsx | 8 +- .../top_nav/app_menu_actions/get_inspect.tsx | 4 +- .../app_menu_actions/get_new_search.tsx | 4 +- .../app_menu_actions/get_open_search.tsx | 4 +- .../top_nav/app_menu_actions/get_share.tsx | 4 +- .../app_menu_actions/run_app_menu_action.tsx | 13 +-- 8 files changed, 119 insertions(+), 54 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index 75cbc8f1f65bf..405919501317b 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -9,26 +9,32 @@ import { AppMenuItem, - AppMenuAction, - AppMenuActionSubmenu, + AppMenuItemPrimary, + AppMenuItemSecondary, + AppMenuItemCustom, + AppMenuActionSecondary, AppMenuActionId, AppMenuActionType, + AppMenuActionSubmenuCustom, + AppMenuActionSubmenuSecondary, + AppMenuActionBase, + AppMenuActionSubmenuBase, } from './types'; export class AppMenuRegistry { private readonly appMenuItems: AppMenuItem[]; - constructor(primaryAndSecondaryActions: AppMenuItem[]) { + constructor(primaryAndSecondaryActions: Array) { this.appMenuItems = assignOrderToActions(primaryAndSecondaryActions); } - public registerCustomAction(appMenuItem: AppMenuAction | AppMenuActionSubmenu) { + public registerCustomAction(appMenuItem: AppMenuItemCustom) { this.appMenuItems.push(appMenuItem); } - public registerCustomActionUnderAlerts(appMenuItem: AppMenuAction) { + public registerCustomActionUnderAlerts(appMenuItem: AppMenuActionSecondary) { const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); - if (alertsMenuItem && isAppMenuActionSubmenu(alertsMenuItem)) { + if (alertsMenuItem && isAppMenuActionSubmenuSecondary(alertsMenuItem)) { alertsMenuItem.actions.push(appMenuItem); } } @@ -48,30 +54,61 @@ export class AppMenuRegistry { } } -function isAppMenuActionSubmenu(appMenuItem: AppMenuItem): appMenuItem is AppMenuActionSubmenu { - return 'actions' in appMenuItem; +function isAppMenuActionSubmenu( + appMenuItem: AppMenuItem +): appMenuItem is AppMenuActionSubmenuSecondary | AppMenuActionSubmenuCustom { + return 'actions' in appMenuItem && Array.isArray(appMenuItem.actions); +} + +function isAppMenuActionSubmenuSecondary( + appMenuItem: AppMenuItem +): appMenuItem is AppMenuActionSubmenuSecondary { + return isAppMenuActionSubmenu(appMenuItem) && appMenuItem.type === AppMenuActionType.secondary; } const FALLBACK_ORDER = Number.MAX_SAFE_INTEGER; -function sortByOrder(a: AppMenuItem, b: AppMenuItem): number { +function sortByOrder(a: T, b: T): number { return (a.order ?? FALLBACK_ORDER) - (b.order ?? FALLBACK_ORDER); } +function getAppMenuSubmenuWithSortedItemsByOrder< + T extends AppMenuActionSubmenuBase = AppMenuActionSubmenuSecondary | AppMenuActionSubmenuCustom +>(appMenuItem: T): T { + return { + ...appMenuItem, + actions: [...appMenuItem.actions].sort(sortByOrder), + }; +} + function sortAppMenuItemsByOrder(appMenuItems: AppMenuItem[]): AppMenuItem[] { const sortedAppMenuItems = [...appMenuItems].sort(sortByOrder); return sortedAppMenuItems.map((appMenuItem) => { if (isAppMenuActionSubmenu(appMenuItem)) { - const popoverWithSortedActions: AppMenuActionSubmenu = { - ...appMenuItem, - actions: [...appMenuItem.actions].sort(sortByOrder), - }; - return popoverWithSortedActions; + return getAppMenuSubmenuWithSortedItemsByOrder(appMenuItem); } return appMenuItem; }); } +function getAppMenuSubmenuWithAssignedOrder< + T extends AppMenuActionSubmenuBase = AppMenuActionSubmenuSecondary | AppMenuActionSubmenuCustom +>(appMenuItem: T, order: number): T { + let orderInSubmenu = 0; + const actionsWithOrder = appMenuItem.actions.map((action) => { + orderInSubmenu = orderInSubmenu + 100; + return { + ...action, + order: action.order ?? orderInSubmenu, + }; + }); + return { + ...appMenuItem, + order: appMenuItem.order ?? order, + actions: actionsWithOrder, + }; +} + /** * All primary and secondary actions by default get order 100, 200, 300,... assigned to them. * Same for actions under a submenu. @@ -82,19 +119,7 @@ function assignOrderToActions(appMenuItems: AppMenuItem[]): AppMenuItem[] { return appMenuItems.map((appMenuItem) => { order = order + 100; if (isAppMenuActionSubmenu(appMenuItem)) { - let orderInPopover = 0; - const actionsWithOrder = appMenuItem.actions.map((action) => { - orderInPopover = orderInPopover + 100; - return { - ...action, - order: action.order ?? orderInPopover, - }; - }); - return { - ...appMenuItem, - order: appMenuItem.order ?? order, - actions: actionsWithOrder, - }; + return getAppMenuSubmenuWithAssignedOrder(appMenuItem, order); } return { ...appMenuItem, diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index ff7b57e234741..73d3eb257cdb5 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -41,23 +41,31 @@ export enum AppMenuActionType { custom = 'custom', } -interface AppMenuActionBase { +export interface AppMenuActionBase { readonly id: AppMenuActionId | string; - readonly order?: number; + readonly order?: number | undefined; } /** - * A normal menu action + * A secondary menu action */ -export interface AppMenuAction extends AppMenuActionBase { - readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; +export interface AppMenuActionSecondary extends AppMenuActionBase { + readonly type: AppMenuActionType.secondary; readonly controlProps: AppMenuControlProps; } /** - * A menu action with icon only + * A custom menu action */ -export interface AppMenuActionIconOnly extends AppMenuActionBase { +export interface AppMenuActionCustom extends AppMenuActionBase { + readonly type: AppMenuActionType.custom; + readonly controlProps: AppMenuControlProps; +} + +/** + * A primary menu action (with icon only) + */ +export interface AppMenuActionPrimary extends AppMenuActionBase { readonly type: AppMenuActionType.primary; readonly controlProps: AppMenuControlIconOnlyProps; } @@ -65,12 +73,39 @@ export interface AppMenuActionIconOnly extends AppMenuActionBase { /** * A menu action which opens a submenu with more actions */ -export interface AppMenuActionSubmenu extends AppMenuActionBase { +export interface AppMenuActionSubmenuBase + extends AppMenuActionBase { + readonly type: T extends AppMenuActionSecondary + ? AppMenuActionType.secondary + : AppMenuActionType.custom; readonly label: TopNavMenuData['label']; readonly description?: TopNavMenuData['description']; readonly testId?: TopNavMenuData['testId']; - readonly type: AppMenuActionType.secondary | AppMenuActionType.custom; - readonly actions: AppMenuAction[]; + readonly actions: T[]; } -export type AppMenuItem = AppMenuActionSubmenu | AppMenuAction | AppMenuActionIconOnly; +/** + * A menu action which opens a submenu with more secondary actions + */ +export type AppMenuActionSubmenuSecondary = AppMenuActionSubmenuBase; +/** + * A menu action which opens a submenu with more custom actions + */ +export type AppMenuActionSubmenuCustom = AppMenuActionSubmenuBase; + +/** + * A primary menu item can only have an icon + */ +export type AppMenuItemPrimary = AppMenuActionPrimary; +/** + * A secondary menu item can have only a label or a submenu + */ +export type AppMenuItemSecondary = AppMenuActionSecondary | AppMenuActionSubmenuSecondary; +/** + * A custom menu item can have only a label or a submenu + */ +export type AppMenuItemCustom = AppMenuActionCustom | AppMenuActionSubmenuCustom; +/** + * A menu item can be primary, secondary or custom + */ +export type AppMenuItem = AppMenuItemPrimary | AppMenuItemSecondary | AppMenuItemCustom; diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index 7ac613fcadd75..db545782c4378 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -10,7 +10,11 @@ import React, { useCallback, useMemo } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; -import { AppMenuActionId, AppMenuActionType, AppMenuActionSubmenu } from '@kbn/discover-utils'; +import { + AppMenuActionId, + AppMenuActionType, + AppMenuActionSubmenuSecondary, +} from '@kbn/discover-utils'; import { AlertConsumers, ES_QUERY_ID, @@ -100,7 +104,7 @@ export const getAlertsAppMenuItem = ({ }: { getDiscoverParams: () => AppMenuDiscoverParams; stateContainer: DiscoverStateContainer; -}): AppMenuActionSubmenu => { +}): AppMenuActionSubmenuSecondary => { const { dataView, services, isEsqlMode } = getDiscoverParams(); const timeField = getTimeField(dataView); const hasTimeFieldName = !isEsqlMode ? Boolean(dataView?.timeFieldName) : Boolean(timeField); diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx index ddd531fc81d3f..5943f598c9aef 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_inspect.tsx @@ -7,14 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuActionId, AppMenuActionType, AppMenuAction } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionSecondary } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; export const getInspectAppMenuItem = ({ onOpenInspector, }: { onOpenInspector: () => void; -}): AppMenuAction => { +}): AppMenuActionSecondary => { return { id: AppMenuActionId.inspect, type: AppMenuActionType.secondary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx index 741f7d65c06f7..b67f14f31c56a 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_new_search.tsx @@ -7,14 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuActionId, AppMenuActionType, AppMenuActionIconOnly } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionPrimary } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; export const getNewSearchAppMenuItem = ({ onNewSearch, }: { onNewSearch: () => void; -}): AppMenuActionIconOnly => { +}): AppMenuActionPrimary => { return { id: AppMenuActionId.new, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx index bb7791f578831..e8f6c5448d602 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_open_search.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { AppMenuActionId, AppMenuActionType, AppMenuActionIconOnly } from '@kbn/discover-utils'; +import { AppMenuActionId, AppMenuActionType, AppMenuActionPrimary } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { OpenSearchPanel } from '../open_search_panel'; @@ -16,7 +16,7 @@ export const getOpenSearchAppMenuItem = ({ onOpenSavedSearch, }: { onOpenSavedSearch: (savedSearchId: string) => void; -}): AppMenuActionIconOnly => { +}): AppMenuActionPrimary => { return { id: AppMenuActionId.open, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index faa467edb9e3c..55a5aaec01aff 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { AppMenuActionIconOnly, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; +import { AppMenuActionPrimary, AppMenuActionId, AppMenuActionType } from '@kbn/discover-utils'; import { omit } from 'lodash'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { i18n } from '@kbn/i18n'; @@ -22,7 +22,7 @@ export const getShareAppMenuItem = ({ }: { stateContainer: DiscoverStateContainer; getDiscoverParams: () => AppMenuDiscoverParams; -}): AppMenuActionIconOnly => { +}): AppMenuActionPrimary => { return { id: AppMenuActionId.share, type: AppMenuActionType.primary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx index f9cfefb5cc6b2..2f2a69b955f29 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx @@ -22,9 +22,10 @@ import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { - AppMenuAction, - AppMenuActionIconOnly, - AppMenuActionSubmenu, + AppMenuActionSecondary, + AppMenuActionPrimary, + AppMenuActionSubmenuSecondary, + AppMenuActionSubmenuCustom, } from '@kbn/discover-utils'; import type { DiscoverServices } from '../../../../../build_services'; @@ -32,7 +33,7 @@ const container = document.createElement('div'); let isOpen = false; interface AppMenuActionsMenuPopoverProps { - appMenuItem: AppMenuActionSubmenu; + appMenuItem: AppMenuActionSubmenuSecondary | AppMenuActionSubmenuCustom; anchorElement: HTMLElement; services: DiscoverServices; onClose: () => void; @@ -114,7 +115,7 @@ export function runAppMenuPopoverAction({ anchorElement, services, }: { - appMenuItem: AppMenuActionSubmenu; + appMenuItem: AppMenuActionSubmenuSecondary | AppMenuActionSubmenuCustom; anchorElement: HTMLElement; services: DiscoverServices; }) { @@ -146,7 +147,7 @@ export async function runAppMenuAction({ anchorElement, services, }: { - appMenuItem: AppMenuAction | AppMenuActionIconOnly; + appMenuItem: AppMenuActionSecondary | AppMenuActionPrimary; anchorElement: HTMLElement; services: DiscoverServices; }) { From e387f50e6153ae2ca6f5eeb17be8ed1b80c0bb3a Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 11 Oct 2024 15:53:23 +0200 Subject: [PATCH 24/61] [Discover] Update types --- .../components/app_menu/app_menu_registry.ts | 4 +- .../src/components/app_menu/types.ts | 4 +- .../app_menu_actions/run_app_menu_action.tsx | 3 +- .../components/top_nav/use_top_nav_links.tsx | 84 ++++++++++--------- 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index 405919501317b..c5da0a10d347c 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -12,13 +12,13 @@ import { AppMenuItemPrimary, AppMenuItemSecondary, AppMenuItemCustom, - AppMenuActionSecondary, AppMenuActionId, AppMenuActionType, AppMenuActionSubmenuCustom, AppMenuActionSubmenuSecondary, AppMenuActionBase, AppMenuActionSubmenuBase, + AppMenuActionCustom, } from './types'; export class AppMenuRegistry { @@ -32,7 +32,7 @@ export class AppMenuRegistry { this.appMenuItems.push(appMenuItem); } - public registerCustomActionUnderAlerts(appMenuItem: AppMenuActionSecondary) { + public registerCustomActionUnderAlerts(appMenuItem: AppMenuActionCustom) { const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); if (alertsMenuItem && isAppMenuActionSubmenuSecondary(alertsMenuItem)) { alertsMenuItem.actions.push(appMenuItem); diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 73d3eb257cdb5..7a6a62a4fc097 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -81,7 +81,9 @@ export interface AppMenuActionSubmenuBase + : AppMenuActionCustom[]; } /** diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx index 2f2a69b955f29..a9b80102b5e85 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx @@ -24,6 +24,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { AppMenuActionSecondary, AppMenuActionPrimary, + AppMenuActionCustom, AppMenuActionSubmenuSecondary, AppMenuActionSubmenuCustom, } from '@kbn/discover-utils'; @@ -147,7 +148,7 @@ export async function runAppMenuAction({ anchorElement, services, }: { - appMenuItem: AppMenuActionSecondary | AppMenuActionPrimary; + appMenuItem: AppMenuActionPrimary | AppMenuActionSecondary | AppMenuActionCustom; anchorElement: HTMLElement; services: DiscoverServices; }) { diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx index e3ed17184a68e..9e406445f933a 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/use_top_nav_links.tsx @@ -13,7 +13,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { AppMenuItem, AppMenuRegistry } from '@kbn/discover-utils'; +import { AppMenuItemPrimary, AppMenuItemSecondary, AppMenuRegistry } from '@kbn/discover-utils'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { DiscoverServices } from '../../../../build_services'; import { onSaveSearch } from './on_save_search'; @@ -68,45 +68,49 @@ export const useTopNavLinks = ({ const defaultMenu = topNavCustomization?.defaultMenu; - const appMenuPrimaryAndSecondaryItems: AppMenuItem[] = useMemo(() => { - const items: AppMenuItem[] = []; - if (!defaultMenu?.inspectItem?.disabled) { - const inspectAppMenuItem = getInspectAppMenuItem({ onOpenInspector }); - items.push(inspectAppMenuItem); - } - - if ( - services.triggersActionsUi && - services.capabilities.management?.insightsAndAlerting?.triggersActions && - !defaultMenu?.alertsItem?.disabled - ) { - const alertsAppMenuItem = getAlertsAppMenuItem({ getDiscoverParams, stateContainer: state }); - items.push(alertsAppMenuItem); - } - - if (!defaultMenu?.newItem?.disabled) { - const newSearchMenuItem = getNewSearchAppMenuItem({ - onNewSearch: () => { - services.locator.navigate({}); - }, - }); - items.push(newSearchMenuItem); - } - - if (!defaultMenu?.openItem?.disabled) { - const openSearchMenuItem = getOpenSearchAppMenuItem({ - onOpenSavedSearch: state.actions.onOpenSavedSearch, - }); - items.push(openSearchMenuItem); - } - - if (!defaultMenu?.shareItem?.disabled) { - const shareAppMenuItem = getShareAppMenuItem({ getDiscoverParams, stateContainer: state }); - items.push(shareAppMenuItem); - } - - return items; - }, [getDiscoverParams, state, services, defaultMenu, onOpenInspector]); + const appMenuPrimaryAndSecondaryItems: Array = + useMemo(() => { + const items: Array = []; + if (!defaultMenu?.inspectItem?.disabled) { + const inspectAppMenuItem = getInspectAppMenuItem({ onOpenInspector }); + items.push(inspectAppMenuItem); + } + + if ( + services.triggersActionsUi && + services.capabilities.management?.insightsAndAlerting?.triggersActions && + !defaultMenu?.alertsItem?.disabled + ) { + const alertsAppMenuItem = getAlertsAppMenuItem({ + getDiscoverParams, + stateContainer: state, + }); + items.push(alertsAppMenuItem); + } + + if (!defaultMenu?.newItem?.disabled) { + const newSearchMenuItem = getNewSearchAppMenuItem({ + onNewSearch: () => { + services.locator.navigate({}); + }, + }); + items.push(newSearchMenuItem); + } + + if (!defaultMenu?.openItem?.disabled) { + const openSearchMenuItem = getOpenSearchAppMenuItem({ + onOpenSavedSearch: state.actions.onOpenSavedSearch, + }); + items.push(openSearchMenuItem); + } + + if (!defaultMenu?.shareItem?.disabled) { + const shareAppMenuItem = getShareAppMenuItem({ getDiscoverParams, stateContainer: state }); + items.push(shareAppMenuItem); + } + + return items; + }, [getDiscoverParams, state, services, defaultMenu, onOpenInspector]); const appMenuRegistry = useMemo( () => new AppMenuRegistry(appMenuPrimaryAndSecondaryItems), From 48faa35c2295ad82cfa02978e693df4d007332d7 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 11 Oct 2024 17:21:30 +0200 Subject: [PATCH 25/61] [Discover] Add horizontal rule support --- .../src/components/app_menu/types.ts | 12 ++- .../top_nav/app_menu_actions/get_alerts.tsx | 6 +- .../app_menu_actions/run_app_menu_action.tsx | 88 ++++++++++--------- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index 7a6a62a4fc097..e54e988d4b551 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -39,6 +39,7 @@ export enum AppMenuActionType { primary = 'primary', secondary = 'secondary', custom = 'custom', + submenuHorizontalRule = 'submenuHorizontalRule', } export interface AppMenuActionBase { @@ -70,6 +71,13 @@ export interface AppMenuActionPrimary extends AppMenuActionBase { readonly controlProps: AppMenuControlIconOnlyProps; } +/** + * A horizontal rule between menu items + */ +export interface AppMenuActionSubmenuHorizontalRule extends AppMenuActionBase { + type: AppMenuActionType.submenuHorizontalRule; +} + /** * A menu action which opens a submenu with more actions */ @@ -82,8 +90,8 @@ export interface AppMenuActionSubmenuBase - : AppMenuActionCustom[]; + ? Array + : Array; } /** diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index db545782c4378..6d1eab07a1e97 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -12,8 +12,8 @@ import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; import { AppMenuActionId, - AppMenuActionType, AppMenuActionSubmenuSecondary, + AppMenuActionType, } from '@kbn/discover-utils'; import { AlertConsumers, @@ -145,6 +145,10 @@ export const getAlertsAppMenuItem = ({ }, }, }, + { + id: 'alertsDivider', + type: AppMenuActionType.submenuHorizontalRule, + }, { id: 'manageRulesAndConnectors', type: AppMenuActionType.secondary, diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx index a9b80102b5e85..76985432053c7 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.tsx @@ -18,15 +18,21 @@ import React, { useCallback, useState } from 'react'; import ReactDOM from 'react-dom'; -import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; +import { + EuiContextMenuPanel, + EuiContextMenuItem, + EuiHorizontalRule, + EuiWrappingPopover, +} from '@elastic/eui'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import type { - AppMenuActionSecondary, - AppMenuActionPrimary, +import { AppMenuActionCustom, - AppMenuActionSubmenuSecondary, + AppMenuActionPrimary, + AppMenuActionSecondary, AppMenuActionSubmenuCustom, + AppMenuActionSubmenuSecondary, + AppMenuActionType, } from '@kbn/discover-utils'; import type { DiscoverServices } from '../../../../../build_services'; @@ -52,39 +58,41 @@ export const AppMenuActionsMenuPopover: React.FC anchorElement?.focus(); }, [anchorElement, originalOnClose]); - const panels = [ - { - id: appMenuItem.id, - name: appMenuItem.label, - items: appMenuItem.actions.map((action) => { - const controlProps = action.controlProps; - - return { - name: controlProps.label, - disabled: - typeof controlProps.disableButton === 'function' - ? controlProps.disableButton() - : Boolean(controlProps.disableButton), - onClick: async () => { - const result = await controlProps.onClick?.({ - anchorElement, - onFinishAction: onClose, - }); - - if (result) { - setNestedContent(result); - } - }, - href: controlProps.href, - ['data-test-subj']: controlProps.testId, - toolTipContent: - typeof controlProps.tooltip === 'function' - ? controlProps.tooltip() - : controlProps.tooltip, - }; - }), - }, - ]; + const items = appMenuItem.actions.map((action) => { + if (action.type === AppMenuActionType.submenuHorizontalRule) { + return ; + } + + const controlProps = action.controlProps; + + return ( + { + const result = await controlProps.onClick?.({ + anchorElement, + onFinishAction: onClose, + }); + + if (result) { + setNestedContent(result); + } + }} + > + {controlProps.label} + + ); + }); return ( <> @@ -94,9 +102,9 @@ export const AppMenuActionsMenuPopover: React.FC button={anchorElement} closePopover={onClose} isOpen={!nestedContent} - panelPaddingSize="s" + panelPaddingSize="none" > - + ); From 82deba65936bc54c815d4aca342e6f92baead089 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 11 Oct 2024 17:25:12 +0200 Subject: [PATCH 26/61] [Discover] Add comments --- .../example/example_data_source_profile/profile.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx index ef44b870737de..1b17398d68856 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx @@ -75,6 +75,10 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi }, getAppMenu: (prev) => (params) => { const prevValue = prev(params); + + // what is available via params: + // const { dataView, services, isEsqlMode, adHocDataViews, onUpdateAdHocDataViews } = params; + return { appMenuRegistry: (registry) => { registry.registerCustomAction({ @@ -111,6 +115,8 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi controlProps: { label: 'Custom 2', onClick: ({ onFinishAction }) => { + // This is an example of a custom action that opens a flyout or any other custom modal + // To do so, simply return a React element and call onFinishAction when you're done return (
Example custom action 2 clicked
From ab3c1719b1070c3a5a09376c07ac9cf486939a42 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 14 Oct 2024 15:41:39 +0200 Subject: [PATCH 27/61] [Discover] Change how submenu items are registered --- .../components/app_menu/app_menu_registry.ts | 77 +++++++++++++------ .../example_data_source_profile/profile.tsx | 47 +++++++---- .../example/example_root_pofile/profile.tsx | 39 +++++++++- .../public/top_nav_menu/top_nav_menu_item.tsx | 5 +- 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index c5da0a10d347c..0e2ee886b8743 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -12,45 +12,82 @@ import { AppMenuItemPrimary, AppMenuItemSecondary, AppMenuItemCustom, - AppMenuActionId, AppMenuActionType, AppMenuActionSubmenuCustom, AppMenuActionSubmenuSecondary, AppMenuActionBase, AppMenuActionSubmenuBase, AppMenuActionCustom, + AppMenuActionSubmenuHorizontalRule, } from './types'; export class AppMenuRegistry { - private readonly appMenuItems: AppMenuItem[]; + private appMenuItems: AppMenuItem[]; + private customSubmenuItemsBySubmenuId: Map< + string, + Array + >; constructor(primaryAndSecondaryActions: Array) { this.appMenuItems = assignOrderToActions(primaryAndSecondaryActions); + this.customSubmenuItemsBySubmenuId = new Map(); + } + + public isActionRegistered(appMenuItemId: string) { + return ( + this.appMenuItems.some((item) => { + if (item.id === appMenuItemId) { + return true; + } + if (isAppMenuActionSubmenu(item)) { + return item.actions.some((submenuItem) => submenuItem.id === appMenuItemId); + } + return false; + }) || + [...this.customSubmenuItemsBySubmenuId.values()].some((submenuItems) => + submenuItems.some((item) => item.id === appMenuItemId) + ) + ); } public registerCustomAction(appMenuItem: AppMenuItemCustom) { - this.appMenuItems.push(appMenuItem); + this.appMenuItems = [ + ...this.appMenuItems.filter((item) => item.id !== appMenuItem.id), + appMenuItem, + ]; } - public registerCustomActionUnderAlerts(appMenuItem: AppMenuActionCustom) { - const alertsMenuItem = this.appMenuItems.find((item) => item.id === AppMenuActionId.alerts); - if (alertsMenuItem && isAppMenuActionSubmenuSecondary(alertsMenuItem)) { - alertsMenuItem.actions.push(appMenuItem); + public registerCustomActionUnderSubmenu(submenuId: string, appMenuItem: AppMenuActionCustom) { + this.customSubmenuItemsBySubmenuId.set(submenuId, [ + ...(this.customSubmenuItemsBySubmenuId.get(submenuId) ?? []).filter( + (item) => item.id !== appMenuItem.id + ), + appMenuItem, + ]); + } + + private getSortedItemsForType(type: AppMenuActionType) { + const actions = this.appMenuItems.filter((item) => item.type === type); + + // enrich submenus with custom actions + if (type === AppMenuActionType.secondary || type === AppMenuActionType.custom) { + [...this.customSubmenuItemsBySubmenuId.entries()].forEach(([submenuId, customActions]) => { + const submenuParentItem = actions.find((item) => item.id === submenuId); + if (submenuParentItem && isAppMenuActionSubmenu(submenuParentItem)) { + submenuParentItem.actions.push(...customActions); + } + }); } + + return sortAppMenuItemsByOrder(actions); } public getSortedItems() { - const primaryActions = sortAppMenuItemsByOrder( - this.appMenuItems.filter((item) => item.type === AppMenuActionType.primary) - ); - const secondaryActions = sortAppMenuItemsByOrder( - this.appMenuItems.filter((item) => item.type === AppMenuActionType.secondary) - ); - const customActions = sortAppMenuItemsByOrder( - this.appMenuItems.filter((item) => item.type === AppMenuActionType.custom) - ); + const primaryItems = this.getSortedItemsForType(AppMenuActionType.primary); + const secondaryItems = this.getSortedItemsForType(AppMenuActionType.secondary); + const customItems = this.getSortedItemsForType(AppMenuActionType.custom); - return [...customActions, ...secondaryActions, ...primaryActions]; + return [...customItems, ...secondaryItems, ...primaryItems]; } } @@ -60,12 +97,6 @@ function isAppMenuActionSubmenu( return 'actions' in appMenuItem && Array.isArray(appMenuItem.actions); } -function isAppMenuActionSubmenuSecondary( - appMenuItem: AppMenuItem -): appMenuItem is AppMenuActionSubmenuSecondary { - return isAppMenuActionSubmenu(appMenuItem) && appMenuItem.type === AppMenuActionType.secondary; -} - const FALLBACK_ORDER = Number.MAX_SAFE_INTEGER; function sortByOrder(a: T, b: T): number { diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx index 1b17398d68856..e36dae4244c27 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx @@ -8,7 +8,12 @@ */ import { EuiBadge, EuiFlyout } from '@elastic/eui'; -import { AppMenuActionType, getFieldValue, RowControlColumn } from '@kbn/discover-utils'; +import { + AppMenuActionId, + AppMenuActionType, + getFieldValue, + RowControlColumn, +} from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -82,25 +87,25 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi return { appMenuRegistry: (registry) => { registry.registerCustomAction({ - id: 'example-custom-action1', + id: 'example-custom-submenu1', type: AppMenuActionType.custom, - label: 'Custom 1', + label: 'Data source submenu', actions: [ { - id: 'example-custom-action11', + id: 'example-custom-action1', type: AppMenuActionType.custom, controlProps: { - label: 'Custom 11', + label: 'Custom 1', onClick: () => { alert('Example custom action 11 clicked'); }, }, }, { - id: 'example-custom-action12', + id: 'example-custom-action2', type: AppMenuActionType.custom, controlProps: { - label: 'Custom 12', + label: 'Custom 2', onClick: () => { alert('Example custom action 12 clicked'); }, @@ -110,30 +115,44 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi }); registry.registerCustomAction({ - id: 'example-custom-action2', + id: 'example-custom-action3', type: AppMenuActionType.custom, controlProps: { - label: 'Custom 2', + label: 'Custom action', onClick: ({ onFinishAction }) => { // This is an example of a custom action that opens a flyout or any other custom modal // To do so, simply return a React element and call onFinishAction when you're done return ( -
Example custom action 2 clicked
+
Example custom action clicked
); }, }, }); - registry.registerCustomActionUnderAlerts({ - id: 'example-custom-action3', + registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { + id: 'example-custom-action4', type: AppMenuActionType.custom, order: 101, controlProps: { - label: 'Custom 3', + label: 'Custom action under Alerts', + onClick: ({ onFinishAction }) => { + alert('Example Custom action under Alerts clicked'); + onFinishAction(); + }, + }, + }); + + // This submenu was defined in the root profile example_root_pofile/profile.tsx + // and we can add actions to it from the data source profile here + registry.registerCustomActionUnderSubmenu('example-custom-root-submenu1', { + id: 'example-custom-action5', + type: AppMenuActionType.custom, + controlProps: { + label: 'Data source action under root submenu', onClick: ({ onFinishAction }) => { - alert('Example custom action 3 clicked'); + alert('Example Data source action under root submenu clicked'); onFinishAction(); }, }, diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx index 125cb609fb849..6ddb505fc5d25 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx @@ -8,7 +8,7 @@ */ import { EuiBadge } from '@elastic/eui'; -import { getFieldValue } from '@kbn/discover-utils'; +import { AppMenuActionType, getFieldValue } from '@kbn/discover-utils'; import React from 'react'; import { RootProfileProvider, SolutionType } from '../../../profiles'; @@ -28,6 +28,43 @@ export const createExampleRootProfileProvider = (): RootProfileProvider => ({ ); }, }), + getAppMenu: (prev) => (params) => { + const prevValue = prev(params); + + return { + appMenuRegistry: (registry) => { + registry.registerCustomAction({ + id: 'example-custom-root-submenu1', + type: AppMenuActionType.custom, + label: 'Root Submenu', + actions: [ + { + id: 'example-custom-root-action11', + type: AppMenuActionType.custom, + controlProps: { + label: 'Root Custom action 11', + onClick: () => { + alert('Example Root Custom action 11 clicked'); + }, + }, + }, + { + id: 'example-custom-root-action12', + type: AppMenuActionType.custom, + controlProps: { + label: 'Root Custom action 12', + onClick: () => { + alert('Example Root Custom action 12 clicked'); + }, + }, + }, + ], + }); + + return prevValue.appMenuRegistry(registry); + }, + }; + }, }, resolve: (params) => { if (params.solutionNavId != null) { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 058c94732e272..e08de06954712 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -72,7 +72,10 @@ export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean Date: Mon, 14 Oct 2024 20:02:21 +0200 Subject: [PATCH 28/61] [Discover] Limit number of custom items --- .../components/app_menu/app_menu_registry.ts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts index 0e2ee886b8743..c6b0e043e38c9 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.ts @@ -8,20 +8,22 @@ */ import { - AppMenuItem, - AppMenuItemPrimary, - AppMenuItemSecondary, - AppMenuItemCustom, - AppMenuActionType, - AppMenuActionSubmenuCustom, - AppMenuActionSubmenuSecondary, AppMenuActionBase, - AppMenuActionSubmenuBase, AppMenuActionCustom, + AppMenuActionSubmenuBase, + AppMenuActionSubmenuCustom, AppMenuActionSubmenuHorizontalRule, + AppMenuActionSubmenuSecondary, + AppMenuActionType, + AppMenuItem, + AppMenuItemCustom, + AppMenuItemPrimary, + AppMenuItemSecondary, } from './types'; export class AppMenuRegistry { + static CUSTOM_ITEMS_LIMIT = 2; + private appMenuItems: AppMenuItem[]; private customSubmenuItemsBySubmenuId: Map< string, @@ -52,7 +54,10 @@ export class AppMenuRegistry { public registerCustomAction(appMenuItem: AppMenuItemCustom) { this.appMenuItems = [ - ...this.appMenuItems.filter((item) => item.id !== appMenuItem.id), + ...this.appMenuItems.filter( + // avoid duplicates and other items override + (item) => !(item.id === appMenuItem.id && item.type === AppMenuActionType.custom) + ), appMenuItem, ]; } @@ -60,14 +65,20 @@ export class AppMenuRegistry { public registerCustomActionUnderSubmenu(submenuId: string, appMenuItem: AppMenuActionCustom) { this.customSubmenuItemsBySubmenuId.set(submenuId, [ ...(this.customSubmenuItemsBySubmenuId.get(submenuId) ?? []).filter( - (item) => item.id !== appMenuItem.id + // avoid duplicates and other items override + (item) => !(item.id === appMenuItem.id && item.type === AppMenuActionType.custom) ), appMenuItem, ]); } private getSortedItemsForType(type: AppMenuActionType) { - const actions = this.appMenuItems.filter((item) => item.type === type); + let actions = this.appMenuItems.filter((item) => item.type === type); + + if (type === AppMenuActionType.custom && actions.length > AppMenuRegistry.CUSTOM_ITEMS_LIMIT) { + // apply the limitation on how many custom items can be shown + actions = actions.slice(0, AppMenuRegistry.CUSTOM_ITEMS_LIMIT); + } // enrich submenus with custom actions if (type === AppMenuActionType.secondary || type === AppMenuActionType.custom) { From 3dde34bf103f7dedb36b2446cbb6a36c86fbfffd Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 15 Oct 2024 12:00:09 +0200 Subject: [PATCH 29/61] [Discover] Update share icon --- .../main/components/top_nav/app_menu_actions/get_share.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index 55a5aaec01aff..9d151af03a02e 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -33,7 +33,7 @@ export const getShareAppMenuItem = ({ description: i18n.translate('discover.localMenu.shareSearchDescription', { defaultMessage: 'Share Search', }), - iconType: 'link', + iconType: 'share', testId: 'shareTopNavButton', onClick: async ({ anchorElement }) => { const { dataView, isEsqlMode, services } = getDiscoverParams(); From 2f76f874759b99dd48095e1e0671b2ab0f3e187e Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 15 Oct 2024 12:17:28 +0200 Subject: [PATCH 30/61] [Discover] Update tooltip delay --- .../navigation/public/top_nav_menu/top_nav_menu_item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index e08de06954712..e48bff3147c19 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -69,7 +69,7 @@ export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean const btn = props.iconOnly && props.iconType && !props.isMobileMenu ? ( // icon only buttons are not supported by EuiHeaderLink - + Date: Tue, 15 Oct 2024 13:37:06 +0200 Subject: [PATCH 31/61] [Discover] Update top nav gap --- .../application/main/components/top_nav/discover_topnav.tsx | 2 ++ .../public/application/main/components/top_nav/top_nav.scss | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/top_nav.scss diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 45e8755ca3156..ce0fead4605d9 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -25,6 +25,7 @@ import { useAppStateSelector } from '../../state_management/discover_app_state_c import { useDiscoverTopNav } from './use_discover_topnav'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { ESQLToDataViewTransitionModal } from './esql_dataview_transition'; +import './top_nav.scss'; export interface DiscoverTopNavProps { savedQuery?: string; @@ -193,6 +194,7 @@ export const DiscoverTopNav = ({ badges: topNavBadges, config: topNavMenu, setMenuMountPoint: setHeaderActionMenu, + className: 'dscTopNav', }; }, [ setHeaderActionMenu, diff --git a/src/plugins/discover/public/application/main/components/top_nav/top_nav.scss b/src/plugins/discover/public/application/main/components/top_nav/top_nav.scss new file mode 100644 index 0000000000000..a8eb63905b210 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/top_nav.scss @@ -0,0 +1,3 @@ +.dscTopNav .euiHeaderLinks__list { + gap: $euiSizeXS; +} From fa0372f7b70f3a6aa82fb3bbcd50fdfe64bfd3a0 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 16 Oct 2024 14:28:14 +0200 Subject: [PATCH 32/61] [Discover] Add top nav tests --- .../top_nav_menu_item.test.tsx.snap | 18 ++++++++++++++++++ .../top_nav_menu/top_nav_menu_item.test.tsx | 18 ++++++++++++++++++ .../public/top_nav_menu/top_nav_menu_item.tsx | 7 ++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu_item.test.tsx.snap b/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu_item.test.tsx.snap index ab174c6d00102..4edea39255c04 100644 --- a/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu_item.test.tsx.snap +++ b/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu_item.test.tsx.snap @@ -1,5 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TopNavMenu Should render an icon-only item 1`] = ` + + + +`; + exports[`TopNavMenu Should render emphasized item which should be clickable 1`] = ` { const ensureMenuItemDisabled = (data: TopNavMenuData) => { @@ -76,6 +77,23 @@ describe('TopNavMenu', () => { expect(component).toMatchSnapshot(); }); + it('Should render an icon-only item', () => { + const data: TopNavMenuData = { + id: 'test', + label: 'test', + iconType: 'share', + iconOnly: true, + run: jest.fn(), + }; + + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + + const event = { currentTarget: { value: 'a' } }; + component.find(EuiButtonIcon).simulate('click', event); + expect(data.run).toHaveBeenCalledTimes(1); + }); + it('Should render disabled item and it shouldnt be clickable', () => { ensureMenuItemDisabled({ id: 'test', diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index e48bff3147c19..16017fda0ffbc 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { upperFirst, isFunction } from 'lodash'; +import { upperFirst, isFunction, omit } from 'lodash'; import React, { MouseEvent } from 'react'; import { EuiToolTip, @@ -72,10 +72,7 @@ export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean Date: Wed, 16 Oct 2024 14:55:27 +0200 Subject: [PATCH 33/61] [Discover] Add app menu registry tests --- .../app_menu_registry.test.ts.snap | 131 ++++++++++++++++ .../app_menu/app_menu_registry.test.ts | 145 ++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 packages/kbn-discover-utils/src/components/app_menu/__snapshots__/app_menu_registry.test.ts.snap create mode 100644 packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.test.ts diff --git a/packages/kbn-discover-utils/src/components/app_menu/__snapshots__/app_menu_registry.test.ts.snap b/packages/kbn-discover-utils/src/components/app_menu/__snapshots__/app_menu_registry.test.ts.snap new file mode 100644 index 0000000000000..2e8b89b5a6314 --- /dev/null +++ b/packages/kbn-discover-utils/src/components/app_menu/__snapshots__/app_menu_registry.test.ts.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppMenuRegistry should allow to register custom actions 1`] = ` +Array [ + Object { + "controlProps": Object { + "label": "Action Custom", + "onClick": [MockFunction], + }, + "id": "action-custom", + "type": "custom", + }, + Object { + "actions": Array [ + Object { + "controlProps": Object { + "label": "Action Custom Submenu 1", + "onClick": [MockFunction], + }, + "id": "action-custom-submenu-1", + "type": "custom", + }, + ], + "id": "action-custom-submenu", + "label": "Action Custom Submenu", + "type": "custom", + }, + Object { + "controlProps": Object { + "label": "Action 2", + "onClick": [MockFunction], + }, + "id": "action-2", + "order": 200, + "type": "secondary", + }, + Object { + "actions": Array [ + Object { + "controlProps": Object { + "label": "Action 3.1", + "onClick": [MockFunction], + }, + "id": "action-3-1", + "order": 100, + "type": "secondary", + }, + Object { + "controlProps": Object { + "label": "Action 3.2", + "onClick": [MockFunction], + }, + "id": "action-3-2", + "order": 200, + "type": "secondary", + }, + ], + "id": "action-3", + "label": "Action 3", + "order": 300, + "type": "secondary", + }, + Object { + "controlProps": Object { + "label": "Action 1", + "onClick": [MockFunction], + }, + "id": "action-1", + "order": 100, + "type": "primary", + }, +] +`; + +exports[`AppMenuRegistry should allow to register custom actions under submenu 1`] = ` +Array [ + Object { + "controlProps": Object { + "label": "Action 2", + "onClick": [MockFunction], + }, + "id": "action-2", + "order": 200, + "type": "secondary", + }, + Object { + "actions": Array [ + Object { + "controlProps": Object { + "label": "Action 3.1", + "onClick": [MockFunction], + }, + "id": "action-3-1", + "order": 100, + "type": "secondary", + }, + Object { + "controlProps": Object { + "label": "Action Custom", + "onClick": [MockFunction], + }, + "id": "action-custom", + "order": 101, + "type": "custom", + }, + Object { + "controlProps": Object { + "label": "Action 3.2", + "onClick": [MockFunction], + }, + "id": "action-3-2", + "order": 200, + "type": "secondary", + }, + ], + "id": "action-3", + "label": "Action 3", + "order": 300, + "type": "secondary", + }, + Object { + "controlProps": Object { + "label": "Action 1", + "onClick": [MockFunction], + }, + "id": "action-1", + "order": 100, + "type": "primary", + }, +] +`; diff --git a/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.test.ts b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.test.ts new file mode 100644 index 0000000000000..cfd9ded4e66bb --- /dev/null +++ b/packages/kbn-discover-utils/src/components/app_menu/app_menu_registry.test.ts @@ -0,0 +1,145 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AppMenuRegistry } from './app_menu_registry'; +import { AppMenuActionSubmenuSecondary, AppMenuActionType } from './types'; + +describe('AppMenuRegistry', () => { + it('should initialize correctly', () => { + const appMenuRegistry = initializeAppMenuRegistry(); + expect(appMenuRegistry.isActionRegistered('action-1')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-2')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-3')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-3-1')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-3-2')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-n')).toBe(false); + expect(appMenuRegistry.getSortedItems()).toHaveLength(3); + }); + + it('should allow to register custom actions', () => { + const appMenuRegistry = initializeAppMenuRegistry(); + expect(appMenuRegistry.isActionRegistered('action-custom')).toBe(false); + + appMenuRegistry.registerCustomAction({ + id: 'action-custom', + type: AppMenuActionType.custom, + controlProps: { + label: 'Action Custom', + onClick: jest.fn(), + }, + }); + + appMenuRegistry.registerCustomAction({ + id: 'action-custom-submenu', + type: AppMenuActionType.custom, + label: 'Action Custom Submenu', + actions: [ + { + id: 'action-custom-submenu-1', + type: AppMenuActionType.custom, + controlProps: { + label: 'Action Custom Submenu 1', + onClick: jest.fn(), + }, + }, + ], + }); + + expect(appMenuRegistry.isActionRegistered('action-custom')).toBe(true); + expect(appMenuRegistry.isActionRegistered('action-custom-submenu')).toBe(true); + expect(appMenuRegistry.getSortedItems()).toHaveLength(5); + + appMenuRegistry.registerCustomAction({ + id: 'action-custom-extra', + type: AppMenuActionType.custom, + controlProps: { + label: 'Action Custom Extra', + onClick: jest.fn(), + }, + }); + + // should limit the number of custom items + const items = appMenuRegistry.getSortedItems(); + expect(items).toHaveLength(5); + expect(items).toMatchSnapshot(); + }); + + it('should allow to register custom actions under submenu', () => { + const appMenuRegistry = initializeAppMenuRegistry(); + expect(appMenuRegistry.isActionRegistered('action-custom')).toBe(false); + + let items = appMenuRegistry.getSortedItems(); + let submenuItem = items.find((item) => item.id === 'action-3') as AppMenuActionSubmenuSecondary; + expect(items).toHaveLength(3); + expect(submenuItem.actions).toHaveLength(2); + + appMenuRegistry.registerCustomActionUnderSubmenu('action-3', { + id: 'action-custom', + type: AppMenuActionType.custom, + order: 101, + controlProps: { + label: 'Action Custom', + onClick: jest.fn(), + }, + }); + + expect(appMenuRegistry.isActionRegistered('action-custom')).toBe(true); + + items = appMenuRegistry.getSortedItems(); + expect(items).toHaveLength(3); + + submenuItem = items.find((item) => item.id === 'action-3') as AppMenuActionSubmenuSecondary; + expect(submenuItem.actions).toHaveLength(3); + expect(items).toMatchSnapshot(); + }); +}); + +function initializeAppMenuRegistry() { + return new AppMenuRegistry([ + { + id: 'action-1', + type: AppMenuActionType.primary, + controlProps: { + label: 'Action 1', + onClick: jest.fn(), + }, + }, + { + id: 'action-2', + type: AppMenuActionType.secondary, + controlProps: { + label: 'Action 2', + onClick: jest.fn(), + }, + }, + { + id: 'action-3', + type: AppMenuActionType.secondary, + label: 'Action 3', + actions: [ + { + id: 'action-3-1', + type: AppMenuActionType.secondary, + controlProps: { + label: 'Action 3.1', + onClick: jest.fn(), + }, + }, + { + id: 'action-3-2', + type: AppMenuActionType.secondary, + controlProps: { + label: 'Action 3.2', + onClick: jest.fn(), + }, + }, + ], + }, + ]); +} From 934359caed576040e404e2c4de52cb3924e1ddbb Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 16 Oct 2024 15:09:16 +0200 Subject: [PATCH 34/61] [Discover] Add more tests --- .../convert_to_top_nav_item.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.test.ts diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.test.ts b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.test.ts new file mode 100644 index 0000000000000..2fb65563cddfb --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/convert_to_top_nav_item.test.ts @@ -0,0 +1,118 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + AppMenuActionPrimary, + AppMenuActionSecondary, + AppMenuActionSubmenuCustom, + AppMenuActionType, +} from '@kbn/discover-utils'; +import { convertAppMenuItemToTopNavItem } from './convert_to_top_nav_item'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; + +describe('convertAppMenuItemToTopNavItem', () => { + it('should convert a primary AppMenuItem to TopNavMenuData', () => { + const appMenuItem: AppMenuActionPrimary = { + id: 'action-1', + type: AppMenuActionType.primary, + controlProps: { + label: 'Action 1', + testId: 'action-1', + iconType: 'share', + onClick: jest.fn(), + }, + }; + + const topNavItem = convertAppMenuItemToTopNavItem({ + appMenuItem, + services: discoverServiceMock, + }); + + expect(topNavItem).toEqual({ + id: 'action-1', + label: 'Action 1', + description: 'Action 1', + testId: 'action-1', + run: expect.any(Function), + iconType: 'share', + iconOnly: true, + }); + }); + + it('should convert a secondary AppMenuItem to TopNavMenuData', () => { + const appMenuItem: AppMenuActionSecondary = { + id: 'action-2', + type: AppMenuActionType.secondary, + controlProps: { + label: 'Action Secondary', + testId: 'action-secondary', + onClick: jest.fn(), + }, + }; + + const topNavItem = convertAppMenuItemToTopNavItem({ + appMenuItem, + services: discoverServiceMock, + }); + + expect(topNavItem).toEqual({ + id: 'action-2', + label: 'Action Secondary', + description: 'Action Secondary', + testId: 'action-secondary', + run: expect.any(Function), + }); + }); + + it('should convert a custom AppMenuItem to TopNavMenuData', () => { + const appMenuItem: AppMenuActionSubmenuCustom = { + id: 'action-3', + type: AppMenuActionType.custom, + label: 'Action submenu', + testId: 'action-submenu', + actions: [ + { + id: 'action-3-1', + type: AppMenuActionType.custom, + controlProps: { + label: 'Action 3.1', + testId: 'action-3-1', + onClick: jest.fn(), + }, + }, + { + id: 'action-3-2', + type: AppMenuActionType.submenuHorizontalRule, + }, + { + id: 'action-3-3', + type: AppMenuActionType.custom, + controlProps: { + label: 'Action 3.3', + testId: 'action-3-3', + onClick: jest.fn(), + }, + }, + ], + }; + + const topNavItem = convertAppMenuItemToTopNavItem({ + appMenuItem, + services: discoverServiceMock, + }); + + expect(topNavItem).toEqual({ + id: 'action-3', + label: 'Action submenu', + description: 'Action submenu', + testId: 'action-submenu', + run: expect.any(Function), + }); + }); +}); From c0a405beab7885b080baf417bf2c7b2d55a1ab4a Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 16 Oct 2024 15:38:26 +0200 Subject: [PATCH 35/61] [Discover] Add more tests --- .../src/components/app_menu/types.ts | 3 +- .../run_app_menu_action.test.tsx | 120 ++++++++++++++++++ .../app_menu_actions/run_app_menu_action.tsx | 2 +- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.test.tsx diff --git a/packages/kbn-discover-utils/src/components/app_menu/types.ts b/packages/kbn-discover-utils/src/components/app_menu/types.ts index e54e988d4b551..b37e1171c3fa6 100644 --- a/packages/kbn-discover-utils/src/components/app_menu/types.ts +++ b/packages/kbn-discover-utils/src/components/app_menu/types.ts @@ -75,7 +75,8 @@ export interface AppMenuActionPrimary extends AppMenuActionBase { * A horizontal rule between menu items */ export interface AppMenuActionSubmenuHorizontalRule extends AppMenuActionBase { - type: AppMenuActionType.submenuHorizontalRule; + readonly type: AppMenuActionType.submenuHorizontalRule; + readonly testId?: TopNavMenuData['testId']; } /** diff --git a/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.test.tsx new file mode 100644 index 0000000000000..952063317d91c --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/app_menu_actions/run_app_menu_action.test.tsx @@ -0,0 +1,120 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { AppMenuActionSubmenuCustom, AppMenuActionType, AppMenuItem } from '@kbn/discover-utils'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; +import { runAppMenuAction, runAppMenuPopoverAction } from './run_app_menu_action'; + +describe('run app menu actions', () => { + describe('runAppMenuAction', () => { + it('should call the action correctly', () => { + const appMenuItem: AppMenuItem = { + id: 'action-1', + type: AppMenuActionType.primary, + controlProps: { + label: 'Action 1', + testId: 'action-1', + iconType: 'share', + onClick: jest.fn(), + }, + }; + + const anchorElement = document.createElement('div'); + + runAppMenuAction({ + appMenuItem, + anchorElement, + services: discoverServiceMock, + }); + + expect(appMenuItem.controlProps.onClick).toHaveBeenCalled(); + }); + + it('should call the action and render a custom content', async () => { + const appMenuItem: AppMenuItem = { + id: 'action-1', + type: AppMenuActionType.primary, + controlProps: { + label: 'Action 1', + testId: 'action-1', + iconType: 'share', + onClick: jest.fn(({ onFinishAction }) => ( +