diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6e80477e16cd7..35b4b24a655be 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -126,6 +126,8 @@ src/core/packages/capabilities/common @elastic/kibana-core src/core/packages/capabilities/server @elastic/kibana-core src/core/packages/capabilities/server-internal @elastic/kibana-core src/core/packages/capabilities/server-mocks @elastic/kibana-core +src/core/packages/chrome/app-menu/core-chrome-app-menu @elastic/appex-sharedux +src/core/packages/chrome/app-menu/core-chrome-app-menu-components @elastic/appex-sharedux src/core/packages/chrome/browser @elastic/appex-sharedux src/core/packages/chrome/browser-internal @elastic/appex-sharedux src/core/packages/chrome/browser-mocks @elastic/appex-sharedux diff --git a/package.json b/package.json index c0f0ab60de8b5..b1516530d635d 100644 --- a/package.json +++ b/package.json @@ -308,6 +308,8 @@ "@kbn/core-capabilities-common": "link:src/core/packages/capabilities/common", "@kbn/core-capabilities-server": "link:src/core/packages/capabilities/server", "@kbn/core-capabilities-server-internal": "link:src/core/packages/capabilities/server-internal", + "@kbn/core-chrome-app-menu": "link:src/core/packages/chrome/app-menu/core-chrome-app-menu", + "@kbn/core-chrome-app-menu-components": "link:src/core/packages/chrome/app-menu/core-chrome-app-menu-components", "@kbn/core-chrome-browser": "link:src/core/packages/chrome/browser", "@kbn/core-chrome-browser-internal": "link:src/core/packages/chrome/browser-internal", "@kbn/core-chrome-layout": "link:src/core/packages/chrome/layout/core-chrome-layout", diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/README.md b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/README.md new file mode 100644 index 0000000000000..9896be2139e35 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/README.md @@ -0,0 +1,18 @@ +# AppMenuComponent + +`AppMenuComponent` is the standalone component used in chrome app menu. + +## Usage + +```tsx +import React, { useEffect } from 'react'; +import { AppMenuComponent, type AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; + +interface Props { + config: AppMenuConfig; +} + +const Example = ({ config }: Props) => { + return ; +}; +``` diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/index.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/index.ts new file mode 100644 index 0000000000000..71b8722e534d8 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { AppMenuComponent } from './src'; +export { AppMenuItem } from './src'; +export { AppMenuActionButton } from './src'; +export { AppMenuOverflowButton } from './src'; +export { AppMenuPopover } from './src'; +export { AppMenuPopoverActionButtons } from './src'; + +export type { + AppMenuConfig, + AppMenuItemType, + AppMenuSecondaryActionItem, + AppMenuPrimaryActionItem, + AppMenuPopoverItem, + AppMenuSplitButtonProps, +} from './src'; + +export { + APP_MENU_ITEM_LIMIT, + APP_MENU_NOTIFICATION_INDICATOR_LEFT, + APP_MENU_NOTIFICATION_INDICATOR_TOP, +} from './src'; + +export { + getDisplayedItemsAllowedAmount, + getShouldOverflow, + isDisabled, + getTooltip, + mapAppMenuItemToPanelItem, + getAppMenuItems, + getPopoverPanels, + getPopoverActionItems, + getIsSelectedColor, +} from './src'; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/jest.config.js b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/jest.config.js new file mode 100644 index 0000000000000..01b54d1c0bb85 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/jest.config.js @@ -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". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/src/core/packages/chrome/app-menu/core-chrome-app-menu-components'], +}; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/kibana.jsonc b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/kibana.jsonc new file mode 100644 index 0000000000000..dc23dff100a37 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-browser", + "id": "@kbn/core-chrome-app-menu-components", + "owner": "@elastic/appex-sharedux", + "group": "platform", + "visibility": "shared" +} diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/moon.yml b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/moon.yml new file mode 100644 index 0000000000000..85de7e84bdaa0 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/moon.yml @@ -0,0 +1,47 @@ +# This file is generated by the @kbn/moon package. Any manual edits will be erased! +# To extend this, write your extensions/overrides to 'moon.extend.yml' +# then regenerate this file with: 'node scripts/regenerate_moon_projects.js --update --filter @kbn/core-chrome-app-menu-components' + +$schema: https://moonrepo.dev/schemas/project.json +id: '@kbn/core-chrome-app-menu-components' +type: unknown +owners: + defaultOwner: '@elastic/appex-sharedux' +toolchain: + default: node +language: typescript +project: + name: '@kbn/core-chrome-app-menu-components' + description: Moon project for @kbn/core-chrome-app-menu-components + channel: '' + owner: '@elastic/appex-sharedux' + metadata: + sourceRoot: src/core/packages/chrome/app-menu/core-chrome-app-menu-components +dependsOn: + - '@kbn/split-button' + - '@kbn/i18n' +tags: + - shared-browser + - package + - prod + - group-platform + - shared + - jest-unit-tests +fileGroups: + src: + - '**/*.ts' + - '**/*.tsx' + - '!target/**/*' +tasks: + jest: + args: + - '--config' + - $projectRoot/jest.config.js + inputs: + - '@group(src)' + jestCI: + args: + - '--config' + - $projectRoot/jest.config.js + inputs: + - '@group(src)' diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/package.json b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/package.json new file mode 100644 index 0000000000000..1d8ea923ca8d9 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/core-chrome-app-menu-components", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "sideEffects": false +} diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/setup_tests.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/setup_tests.ts new file mode 100644 index 0000000000000..5ebc6d3dac1ca --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/setup_tests.ts @@ -0,0 +1,11 @@ +/* + * 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". + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.test.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.test.tsx similarity index 79% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.test.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.test.tsx index 552fb1f8b3cab..539af8be5a5e0 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.test.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { TopNavMenuBeta } from './top_nav_menu_beta'; -import type { TopNavMenuConfigBeta } from './types'; +import { AppMenuComponent } from './app_menu'; +import type { AppMenuConfig } from '../types'; // Mock useIsWithinBreakpoints to control responsive behavior const mockUseIsWithinBreakpoints = jest.fn(); @@ -22,13 +22,13 @@ jest.mock('@elastic/eui', () => { }; }); -describe('TopNavMenuBeta', () => { +describe('AppMenu', () => { const defaultItems = [ { id: 'item1', label: 'Item 1', run: jest.fn(), iconType: 'gear', order: 1 }, { id: 'item2', label: 'Item 2', run: jest.fn(), iconType: 'search', order: 2 }, ]; - const defaultConfig: TopNavMenuConfigBeta = { + const defaultConfig: AppMenuConfig = { items: defaultItems, }; @@ -43,31 +43,31 @@ describe('TopNavMenuBeta', () => { describe('rendering', () => { it('should return null when config is undefined', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('should return null when config has no items', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('should return null when visible is false', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('should render the top nav menu when config has items', () => { - render(); + render(); expect(screen.getByTestId('top-nav')).toBeInTheDocument(); }); it('should render menu items at xl breakpoint', () => { - render(); + render(); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); @@ -76,7 +76,7 @@ describe('TopNavMenuBeta', () => { describe('action items', () => { it('should render primary action item', () => { - const configWithPrimary: TopNavMenuConfigBeta = { + const configWithPrimary: AppMenuConfig = { primaryActionItem: { id: 'save', label: 'Save', @@ -85,13 +85,13 @@ describe('TopNavMenuBeta', () => { }, }; - render(); + render(); expect(screen.getByText('Save')).toBeInTheDocument(); }); it('should render secondary action item', () => { - const configWithSecondary: TopNavMenuConfigBeta = { + const configWithSecondary: AppMenuConfig = { secondaryActionItem: { id: 'cancel', label: 'Cancel', @@ -100,13 +100,13 @@ describe('TopNavMenuBeta', () => { }, }; - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); }); it('should render both primary and secondary action items', () => { - const configWithBoth: TopNavMenuConfigBeta = { + const configWithBoth: AppMenuConfig = { primaryActionItem: { id: 'save', label: 'Save', @@ -121,7 +121,7 @@ describe('TopNavMenuBeta', () => { }, }; - render(); + render(); expect(screen.getByText('Save')).toBeInTheDocument(); expect(screen.getByText('Cancel')).toBeInTheDocument(); @@ -135,7 +135,7 @@ describe('TopNavMenuBeta', () => { return false; }); - render(); + render(); expect(screen.getByTestId('top-nav-menu-overflow-button')).toBeInTheDocument(); }); @@ -143,7 +143,7 @@ describe('TopNavMenuBeta', () => { it('should render overflow button with all items at small breakpoint', () => { mockUseIsWithinBreakpoints.mockReturnValue(false); - render(); + render(); expect(screen.getByTestId('top-nav-menu-overflow-button')).toBeInTheDocument(); }); @@ -154,7 +154,7 @@ describe('TopNavMenuBeta', () => { return false; }); - render(); + render(); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.tsx similarity index 83% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.tsx index 46e0c0314ce48..3ef9280c15d1b 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu.tsx @@ -9,21 +9,21 @@ import React, { useState } from 'react'; import { EuiHeaderLinks, useIsWithinBreakpoints } from '@elastic/eui'; -import { getTopNavItems } from './utils'; -import { TopNavMenuActionButton } from './top_nav_menu_action_button'; -import { TopNavMenuItem } from './top_nav_menu_item'; -import { TopNavMenuOverflowButton } from './top_nav_menu_overflow_button'; -import type { TopNavMenuConfigBeta } from './types'; +import { getAppMenuItems } from '../utils'; +import { AppMenuActionButton } from './app_menu_action_button'; +import { AppMenuItem } from './app_menu_item'; +import { AppMenuOverflowButton } from './app_menu_overflow_button'; +import type { AppMenuConfig } from '../types'; -export interface TopNavMenuItemsProps { - config?: TopNavMenuConfigBeta; +export interface AppMenuItemsProps { + config?: AppMenuConfig; visible?: boolean; } -const hasNoItems = (config: TopNavMenuConfigBeta) => +const hasNoItems = (config: AppMenuConfig) => !config.items?.length && !config?.primaryActionItem && !config?.secondaryActionItem; -export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) => { +export const AppMenuComponent = ({ config, visible = true }: AppMenuItemsProps) => { const [openPopoverId, setOpenPopoverId] = useState(null); const isBetweenMandXlBreakpoint = useIsWithinBreakpoints(['m', 'l']); const isAboveXlBreakpoint = useIsWithinBreakpoints(['xl']); @@ -43,7 +43,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) className: 'kbnTopNavMenu__wrapper', }; - const { displayedItems, overflowItems, shouldOverflow } = getTopNavItems({ + const { displayedItems, overflowItems, shouldOverflow } = getAppMenuItems({ config, }); @@ -56,7 +56,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) }; const primaryActionComponent = primaryActionItem ? ( - { @@ -67,7 +67,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) ) : undefined; const secondaryActionComponent = secondaryActionItem ? ( - { @@ -80,7 +80,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) if (isBetweenMandXlBreakpoint) { return ( - handlePopoverToggle(showMoreButtonId)} @@ -97,7 +97,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) {displayedItems?.length > 0 && displayedItems.map((menuItem) => ( - ))} {shouldOverflow && ( - handlePopoverToggle(showMoreButtonId)} @@ -121,7 +121,7 @@ export const TopNavMenuBeta = ({ config, visible = true }: TopNavMenuItemsProps) return ( - { +describe('AppMenuActionButton', () => { const defaultProps = { label: 'save', run: jest.fn(), @@ -34,14 +34,14 @@ describe('TopNavMenuActionButton', () => { }); it('should render basic action button', () => { - render(); + render(); expect(screen.getByTestId('test-action-button')).toBeInTheDocument(); expect(screen.getByText('Save')).toBeInTheDocument(); }); it('should call run function when clicked', async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByTestId('test-action-button')); @@ -50,7 +50,7 @@ describe('TopNavMenuActionButton', () => { }); it('should render as split button', () => { - render(); + render(); expect(screen.getByText('Save')).toBeInTheDocument(); expect(screen.getByLabelText('More options')).toBeInTheDocument(); }); @@ -58,7 +58,7 @@ describe('TopNavMenuActionButton', () => { it('should call main run function when primary button is clicked', async () => { const user = userEvent.setup(); render( - { it('should call split button run function when secondary button is clicked', async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByLabelText('More options')); @@ -83,11 +83,7 @@ describe('TopNavMenuActionButton', () => { it('should not attach onClick handler when href is present', () => { render( - + ); const button = screen.getByTestId('test-action-button'); @@ -98,7 +94,7 @@ describe('TopNavMenuActionButton', () => { it('should render disabled button when disableButton is true', () => { render( - + ); expect(screen.getByTestId('test-action-button')).toBeDisabled(); @@ -107,7 +103,7 @@ describe('TopNavMenuActionButton', () => { it('should not call run when button is disabled', async () => { const user = userEvent.setup(); render( - + ); await user.click(screen.getByTestId('test-action-button')); @@ -130,7 +126,7 @@ describe('TopNavMenuActionButton', () => { ], }; - render(); + render(); await user.click(screen.getByTestId('test-action-button')); @@ -149,7 +145,7 @@ describe('TopNavMenuActionButton', () => { }; render( - { }; render( - void; onPopoverClose: () => void; popoverAnchorPosition?: PopoverAnchorPosition; }; -export const TopNavMenuActionButton = (props: TopNavMenuActionButtonProps) => { +export const AppMenuActionButton = (props: AppMenuActionButtonProps) => { const { euiTheme } = useEuiTheme(); const { @@ -70,7 +70,7 @@ export const TopNavMenuActionButton = (props: TopNavMenuActionButtonProps) => { items: splitButtonItems, run: splitButtonRun, ...otherSplitButtonProps - } = splitButtonProps || ({} as TopNavMenuSplitButtonProps); + } = splitButtonProps || ({} as AppMenuSplitButtonProps); const hasItems = items && items.length > 0; const hasSplitItems = splitButtonItems && splitButtonItems.length > 0; @@ -145,8 +145,8 @@ export const TopNavMenuActionButton = (props: TopNavMenuActionButtonProps) => { isSelected={isPopoverOpen} css={splitButtonCss} notificationIndicatorPosition={{ - top: TOP_NAV_MENU_NOTIFICATION_INDICATOR_TOP, - left: TOP_NAV_MENU_NOTIFICATION_INDICATOR_LEFT, + top: APP_MENU_NOTIFICATION_INDICATOR_TOP, + left: APP_MENU_NOTIFICATION_INDICATOR_LEFT, }} notificationIndicatorSize="m" notificationIndicatorColor="primary" @@ -187,7 +187,7 @@ export const TopNavMenuActionButton = (props: TopNavMenuActionButtonProps) => { if (hasItems || hasSplitItems) { return ( - { +describe('AppMenuItem', () => { const defaultProps = { label: 'elastic', run: jest.fn(), @@ -29,20 +29,20 @@ describe('TopNavMenuItem', () => { }); it('should render basic item', () => { - render(); + render(); expect(screen.getByTestId('test-button')).toBeInTheDocument(); expect(screen.getByText('Elastic')).toBeInTheDocument(); }); it('should render as link when href is provided', () => { - render(); + render(); const link = screen.getByTestId('test-link'); expect(link).toHaveAttribute('href', 'http://elastic.co'); }); it('should call run function when clicked', async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByTestId('test-button')); @@ -50,14 +50,14 @@ describe('TopNavMenuItem', () => { }); it('should render disabled item when disableButton is true', () => { - render(); + render(); expect(screen.getByTestId('test-button')).toBeDisabled(); }); it('should not call run when item is disabled', async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByTestId('test-button')); @@ -76,7 +76,7 @@ describe('TopNavMenuItem', () => { items, }; - render(); + render(); await user.click(screen.getByTestId('test-button')); @@ -84,7 +84,7 @@ describe('TopNavMenuItem', () => { }); it('should capitalize the label text', () => { - render(); + render(); expect(screen.getByText('Settings')).toBeInTheDocument(); }); diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_item.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_item.tsx similarity index 90% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_item.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_item.tsx index 8bde63487417d..8e71bbdd0b8ef 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_item.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_item.tsx @@ -11,17 +11,17 @@ import React from 'react'; import { EuiHeaderLink, EuiHideFor, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { upperFirst } from 'lodash'; import { css } from '@emotion/react'; -import { getIsSelectedColor, getTooltip, isDisabled } from './utils'; -import { TopNavMenuPopover } from './top_nav_menu_popover'; -import type { TopNavMenuItemType } from './types'; +import { getIsSelectedColor, getTooltip, isDisabled } from '../utils'; +import { AppMenuPopover } from './app_menu_popover'; +import type { AppMenuItemType } from '../types'; -type TopNavMenuItemBetaProps = TopNavMenuItemType & { +type AppMenuItemProps = AppMenuItemType & { isPopoverOpen: boolean; onPopoverToggle: () => void; onPopoverClose: () => void; }; -export const TopNavMenuItem = ({ +export const AppMenuItem = ({ run, id, htmlId, @@ -40,7 +40,7 @@ export const TopNavMenuItem = ({ popoverWidth, onPopoverToggle, onPopoverClose, -}: TopNavMenuItemBetaProps) => { +}: AppMenuItemProps) => { const { euiTheme } = useEuiTheme(); const itemText = upperFirst(label); @@ -109,7 +109,7 @@ export const TopNavMenuItem = ({ if (hasItems) { return ( - { +describe('AppMenuOverflowButton', () => { const defaultItems = [ { id: 'item1', label: 'Item 1', run: jest.fn(), iconType: 'gear', order: 1 }, { id: 'item2', label: 'Item 2', run: jest.fn(), iconType: 'search', order: 2 }, @@ -30,14 +30,14 @@ describe('TopNavMenuOverflowButton', () => { }); it('should render the overflow button', () => { - render(); + render(); expect(screen.getByTestId('top-nav-menu-overflow-button')).toBeInTheDocument(); }); it('should call onPopoverToggle when clicked', async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByTestId('top-nav-menu-overflow-button')); @@ -45,7 +45,7 @@ describe('TopNavMenuOverflowButton', () => { }); it('should return null when items array is empty', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_overflow_button.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_overflow_button.tsx similarity index 73% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_overflow_button.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_overflow_button.tsx index 44a855a9fe967..84e7cf0b20832 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_overflow_button.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_overflow_button.tsx @@ -11,31 +11,31 @@ import React from 'react'; import { EuiButtonIcon, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { getIsSelectedColor } from './utils'; -import { TopNavMenuPopover } from './top_nav_menu_popover'; +import { getIsSelectedColor } from '../utils'; +import { AppMenuPopover } from './app_menu_popover'; import type { - TopNavMenuItemType, - TopNavMenuPrimaryActionItem, - TopNavMenuSecondaryActionItem, -} from './types'; + AppMenuItemType, + AppMenuPrimaryActionItem, + AppMenuSecondaryActionItem, +} from '../types'; -interface TopNavMenuShowMoreButtonProps { - items: TopNavMenuItemType[]; +interface AppMenuShowMoreButtonProps { + items: AppMenuItemType[]; isPopoverOpen: boolean; - primaryActionItem?: TopNavMenuPrimaryActionItem; - secondaryActionItem?: TopNavMenuSecondaryActionItem; + primaryActionItem?: AppMenuPrimaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; onPopoverToggle: () => void; onPopoverClose: () => void; } -export const TopNavMenuOverflowButton = ({ +export const AppMenuOverflowButton = ({ items, isPopoverOpen, primaryActionItem, secondaryActionItem, onPopoverToggle, onPopoverClose, -}: TopNavMenuShowMoreButtonProps) => { +}: AppMenuShowMoreButtonProps) => { const { euiTheme } = useEuiTheme(); if (items.length === 0) { @@ -60,7 +60,7 @@ export const TopNavMenuOverflowButton = ({ { +describe('AppMenuPopover', () => { const defaultItems = [ { id: 'item1', label: 'Item 1', run: jest.fn(), order: 1 }, { id: 'item2', label: 'Item 2', run: jest.fn(), order: 2 }, @@ -31,40 +31,40 @@ describe('TopNavMenuPopover', () => { }); it('should render the popover with anchor element', () => { - render(); + render(); expect(screen.getByTestId('anchor-button')).toBeInTheDocument(); }); it('should render anchor element even with empty items', () => { - render(); + render(); expect(screen.getByTestId('anchor-button')).toBeInTheDocument(); }); it('should render menu items when popover is open', () => { - render(); + render(); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); }); it('should not render menu items when popover is closed', () => { - render(); + render(); expect(screen.queryByText('Item 1')).not.toBeInTheDocument(); expect(screen.queryByText('Item 2')).not.toBeInTheDocument(); }); it('should wrap anchor in tooltip when tooltipContent is provided', () => { - render(); + render(); const anchorButton = screen.getByTestId('anchor-button'); expect(anchorButton.closest('.euiToolTipAnchor')).toBeInTheDocument(); }); it('should wrap anchor in tooltip when tooltipTitle is provided', () => { - render(); + render(); const anchorButton = screen.getByTestId('anchor-button'); expect(anchorButton.closest('.euiToolTipAnchor')).toBeInTheDocument(); @@ -72,7 +72,7 @@ describe('TopNavMenuPopover', () => { it('should wrap anchor in tooltip when both tooltipContent and tooltipTitle are provided', () => { render( - { }); it('should not wrap anchor in tooltip when neither tooltipContent nor tooltipTitle is provided', () => { - render(); + render(); const anchorButton = screen.getByTestId('anchor-button'); expect(anchorButton.closest('.euiToolTipAnchor')).not.toBeInTheDocument(); @@ -92,7 +92,7 @@ describe('TopNavMenuPopover', () => { it('should apply popoverWidth to the panel style', () => { const { baseElement } = render( - + ); const popoverPanel = baseElement.querySelector('.euiPanel'); diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover.tsx similarity index 83% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover.tsx index c19e2d9c4a21e..94142e0ca1fbf 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover.tsx @@ -10,28 +10,28 @@ import React, { useMemo, type ReactElement } from 'react'; import type { PopoverAnchorPosition } from '@elastic/eui'; import { EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui'; -import { getPopoverPanels, getTooltip } from './utils'; +import { getPopoverPanels, getTooltip } from '../utils'; import type { - TopNavMenuPopoverItem, - TopNavMenuPrimaryActionItem, - TopNavMenuSecondaryActionItem, -} from './types'; + AppMenuPopoverItem, + AppMenuPrimaryActionItem, + AppMenuSecondaryActionItem, +} from '../types'; -interface TopNavContextMenuProps { +interface AppMenuContextMenuProps { tooltipContent?: string | (() => string | undefined); tooltipTitle?: string | (() => string | undefined); anchorElement: ReactElement; - items: TopNavMenuPopoverItem[]; + items: AppMenuPopoverItem[]; isOpen: boolean; popoverWidth?: number; - primaryActionItem?: TopNavMenuPrimaryActionItem; - secondaryActionItem?: TopNavMenuSecondaryActionItem; + primaryActionItem?: AppMenuPrimaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; anchorPosition?: PopoverAnchorPosition; testId?: string; onClose: () => void; } -export const TopNavMenuPopover = ({ +export const AppMenuPopover = ({ items, anchorElement, tooltipContent, @@ -43,7 +43,7 @@ export const TopNavMenuPopover = ({ anchorPosition, testId, onClose, -}: TopNavContextMenuProps) => { +}: AppMenuContextMenuProps) => { const panels = useMemo( () => getPopoverPanels({ items, primaryActionItem, secondaryActionItem }), [items, primaryActionItem, secondaryActionItem] diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.test.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.test.tsx similarity index 79% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.test.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.test.tsx index 127b6ee69697e..431aec2ffb351 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.test.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { TopNavMenuPopoverActionButtons } from './top_nav_menu_popover_action_buttons'; +import { AppMenuPopoverActionButtons } from './app_menu_popover_action_buttons'; -describe('TopNavMenuPopoverActionButtons', () => { +describe('AppMenuPopoverActionButtons', () => { const primaryActionItem = { id: 'save', label: 'Save', @@ -31,38 +31,38 @@ describe('TopNavMenuPopoverActionButtons', () => { }); it('should return null when neither primary nor secondary action item is provided', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('should render container when primary action item is provided', () => { - render(); + render(); expect(screen.getByTestId('top-nav-menu-popover-action-buttons-container')).toBeInTheDocument(); }); it('should render container when secondary action item is provided', () => { - render(); + render(); expect(screen.getByTestId('top-nav-menu-popover-action-buttons-container')).toBeInTheDocument(); }); it('should render primary action button', () => { - render(); + render(); expect(screen.getByText('Save')).toBeInTheDocument(); }); it('should render secondary action button', () => { - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); }); it('should render both primary and secondary action buttons', () => { render( - @@ -74,7 +74,7 @@ describe('TopNavMenuPopoverActionButtons', () => { it('should render secondary action before primary action in DOM order', () => { render( - diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.tsx similarity index 82% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.tsx index 1c701bcf5d7e5..2870e44c07d00 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_popover_action_buttons.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/app_menu_popover_action_buttons.tsx @@ -10,18 +10,18 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { TopNavMenuActionButton } from './top_nav_menu_action_button'; -import type { TopNavMenuPrimaryActionItem, TopNavMenuSecondaryActionItem } from './types'; +import { AppMenuActionButton } from './app_menu_action_button'; +import type { AppMenuPrimaryActionItem, AppMenuSecondaryActionItem } from '../types'; -interface TopNavMenuPopoverActionButtonsProps { - primaryActionItem?: TopNavMenuPrimaryActionItem; - secondaryActionItem?: TopNavMenuSecondaryActionItem; +interface AppMenuPopoverActionButtonsProps { + primaryActionItem?: AppMenuPrimaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; } -export const TopNavMenuPopoverActionButtons = ({ +export const AppMenuPopoverActionButtons = ({ primaryActionItem, secondaryActionItem, -}: TopNavMenuPopoverActionButtonsProps) => { +}: AppMenuPopoverActionButtonsProps) => { const [openPopoverId, setOpenPopoverId] = useState(null); const { euiTheme } = useEuiTheme(); @@ -53,7 +53,7 @@ export const TopNavMenuPopoverActionButtons = ({ > {secondaryActionItem && ( - { @@ -66,7 +66,7 @@ export const TopNavMenuPopoverActionButtons = ({ )} {primaryActionItem && ( - { diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/index.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/index.ts new file mode 100644 index 0000000000000..9ddc53b7803d7 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/components/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { AppMenuComponent } from './app_menu'; +export { AppMenuItem } from './app_menu_item'; +export { AppMenuActionButton } from './app_menu_action_button'; +export { AppMenuOverflowButton } from './app_menu_overflow_button'; +export { AppMenuPopover } from './app_menu_popover'; +export { AppMenuPopoverActionButtons } from './app_menu_popover_action_buttons'; diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/constants.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/constants.ts similarity index 74% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/constants.ts rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/constants.ts index 43a89319610c1..8bda71b4bff4e 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/constants.ts +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/constants.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export const TOP_NAV_MENU_ITEM_LIMIT = 5; -export const TOP_NAV_MENU_NOTIFICATION_INDICATOR_TOP = 2; -export const TOP_NAV_MENU_NOTIFICATION_INDICATOR_LEFT = 25; +export const APP_MENU_ITEM_LIMIT = 5; +export const APP_MENU_NOTIFICATION_INDICATOR_TOP = 2; +export const APP_MENU_NOTIFICATION_INDICATOR_LEFT = 25; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/index.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/index.ts new file mode 100644 index 0000000000000..2f26b14e1ab22 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { AppMenuComponent } from './components'; +export { AppMenuItem } from './components'; +export { AppMenuActionButton } from './components'; +export { AppMenuOverflowButton } from './components'; +export { AppMenuPopover } from './components'; +export { AppMenuPopoverActionButtons } from './components'; + +export type { + AppMenuConfig, + AppMenuItemType, + AppMenuSecondaryActionItem, + AppMenuPrimaryActionItem, + AppMenuPopoverItem, + AppMenuSplitButtonProps, +} from './types'; + +export { + APP_MENU_ITEM_LIMIT, + APP_MENU_NOTIFICATION_INDICATOR_LEFT, + APP_MENU_NOTIFICATION_INDICATOR_TOP, +} from './constants'; + +export { + getDisplayedItemsAllowedAmount, + getShouldOverflow, + isDisabled, + getTooltip, + mapAppMenuItemToPanelItem, + getAppMenuItems, + getPopoverPanels, + getPopoverActionItems, + getIsSelectedColor, +} from './utils'; diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/types.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/types.ts similarity index 85% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/types.ts rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/types.ts index 025d3430e0b70..2cdbc26a43076 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/types.ts +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/types.ts @@ -30,7 +30,7 @@ type BaseSplitProps = Pick< /** * Subset of SplitButtonWithNotificationProps. */ -export type TopNavMenuSplitButtonProps = +export type AppMenuSplitButtonProps = /** * If `items` is provided then `run` shouldn't be, as having items means the button opens a popover. */ @@ -48,14 +48,14 @@ export type TopNavMenuSplitButtonProps = /** * Sub-items to show in a popover when the item is clicked. Only used if `run` is not provided. */ - items: TopNavMenuPopoverItem[]; + items: AppMenuPopoverItem[]; /** * Function to run when the item is clicked. Only used if `items` is not provided. */ run?: never; }); -interface TopNavItemBase { +interface AppMenuItemBase { /** * A unique, internal identifier for the item. */ @@ -106,11 +106,11 @@ interface TopNavItemBase { hidden?: EuiHideForProps['sizes']; } -export type TopNavMenuItemCommon = +export type AppMenuItemCommon = /** * If `items` is provided then `run` shouldn't be, as having items means the button opens a popover. */ - | (TopNavItemBase & { + | (AppMenuItemBase & { /** * Function to run when the item is clicked. Only used if `items` is not provided. */ @@ -124,7 +124,7 @@ export type TopNavMenuItemCommon = */ popoverWidth?: never; }) - | (TopNavItemBase & { + | (AppMenuItemBase & { /** * Function to run when the item is clicked. Only used if `items` is not provided. */ @@ -132,7 +132,7 @@ export type TopNavMenuItemCommon = /** * Sub-items to show in a popover when the item is clicked. Only used if `run` is not provided. */ - items: TopNavMenuPopoverItem[]; + items: AppMenuPopoverItem[]; /** * Width of the popover in pixels. */ @@ -142,7 +142,7 @@ export type TopNavMenuItemCommon = /** * Full item type for use in `config.items` arrays. */ -export type TopNavMenuItemType = TopNavMenuItemCommon & { +export type AppMenuItemType = AppMenuItemCommon & { /** * Order of the item in the menu. Lower numbers appear first. */ @@ -152,10 +152,7 @@ export type TopNavMenuItemType = TopNavMenuItemCommon & { /** * Popover item type for use in `items` arrays. */ -export type TopNavMenuPopoverItem = Omit< - TopNavMenuItemType, - 'iconType' | 'hidden' | 'popoverWidth' -> & { +export type AppMenuPopoverItem = Omit & { /** * The icon type for the item. */ @@ -169,7 +166,7 @@ export type TopNavMenuPopoverItem = Omit< /** * Secondary action button type. Can only be a simple button. */ -export type TopNavMenuSecondaryActionItem = TopNavMenuItemCommon & { +export type AppMenuSecondaryActionItem = AppMenuItemCommon & { /** * The color of the button. */ @@ -187,31 +184,31 @@ export type TopNavMenuSecondaryActionItem = TopNavMenuItemCommon & { /** * Primary action button type. Can be either a simple button or a split button. */ -export type TopNavMenuPrimaryActionItem = +export type AppMenuPrimaryActionItem = /** * The main part of the button should never open a popover. */ - Omit & { + Omit & { /** * Subset of SplitButtonWithNotificationProps. */ - splitButtonProps?: TopNavMenuSplitButtonProps; + splitButtonProps?: AppMenuSplitButtonProps; }; /** - * Configuration object for the TopNavMenuBeta component. + * Configuration object for the AppMenu component. */ -export interface TopNavMenuConfigBeta { +export interface AppMenuConfig { /** * List of menu items to display in the top navigation menu. */ - items?: TopNavMenuItemType[]; + items?: AppMenuItemType[]; /** * Primary action button to display in the top navigation menu. */ - primaryActionItem?: TopNavMenuPrimaryActionItem; + primaryActionItem?: AppMenuPrimaryActionItem; /** * Secondary action button to display in the top navigation menu. */ - secondaryActionItem?: TopNavMenuSecondaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; } diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.test.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.test.tsx similarity index 88% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.test.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.test.tsx index c16d9fa1d1651..f0b9377e7abcd 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.test.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.test.tsx @@ -11,21 +11,21 @@ import type { EuiThemeComputed } from '@elastic/eui'; import { getDisplayedItemsAllowedAmount, getShouldOverflow, - getTopNavItems, - mapTopNavItemToPanelItem, + getAppMenuItems, + mapAppMenuItemToPanelItem, getPopoverActionItems, getPopoverPanels, getIsSelectedColor, } from './utils'; -import { TOP_NAV_MENU_ITEM_LIMIT } from './constants'; -import type { TopNavMenuPopoverItem } from './types'; +import { APP_MENU_ITEM_LIMIT } from './constants'; +import type { AppMenuPopoverItem } from './types'; describe('utils', () => { describe('getDisplayedItemsAllowedAmount', () => { it('should return full limit when no action items', () => { const result = getDisplayedItemsAllowedAmount({}); - expect(result).toBe(TOP_NAV_MENU_ITEM_LIMIT); + expect(result).toBe(APP_MENU_ITEM_LIMIT); }); it('should reduce limit by 1 when primary action item is present', () => { @@ -33,7 +33,7 @@ describe('utils', () => { primaryActionItem: { id: 'save', label: 'Save', run: jest.fn(), iconType: 'save' }, }); - expect(result).toBe(TOP_NAV_MENU_ITEM_LIMIT - 1); + expect(result).toBe(APP_MENU_ITEM_LIMIT - 1); }); it('should reduce limit by 1 when secondary action item is present', () => { @@ -41,7 +41,7 @@ describe('utils', () => { secondaryActionItem: { id: 'cancel', label: 'Cancel', run: jest.fn(), iconType: 'cross' }, }); - expect(result).toBe(TOP_NAV_MENU_ITEM_LIMIT - 1); + expect(result).toBe(APP_MENU_ITEM_LIMIT - 1); }); it('should reduce limit by 2 when both action items are present', () => { @@ -50,7 +50,7 @@ describe('utils', () => { secondaryActionItem: { id: 'cancel', label: 'Cancel', run: jest.fn(), iconType: 'cross' }, }); - expect(result).toBe(TOP_NAV_MENU_ITEM_LIMIT - 2); + expect(result).toBe(APP_MENU_ITEM_LIMIT - 2); }); }); @@ -113,9 +113,9 @@ describe('utils', () => { }); }); - describe('getTopNavItems', () => { + describe('getAppMenuItems', () => { it('should return empty arrays when config has no items', () => { - const result = getTopNavItems({ config: {} }); + const result = getAppMenuItems({ config: {} }); expect(result).toEqual({ displayedItems: [], @@ -130,7 +130,7 @@ describe('utils', () => { { id: '2', label: 'Item 2', run: jest.fn(), iconType: 'gear' as const, order: 2 }, ]; - const result = getTopNavItems({ config: { items } }); + const result = getAppMenuItems({ config: { items } }); expect(result.displayedItems).toHaveLength(2); expect(result.overflowItems).toHaveLength(0); @@ -144,7 +144,7 @@ describe('utils', () => { { id: '2', label: 'Item 2', run: jest.fn(), iconType: 'gear' as const, order: 2 }, ]; - const result = getTopNavItems({ config: { items } }); + const result = getAppMenuItems({ config: { items } }); expect(result.displayedItems[0].id).toBe('1'); expect(result.displayedItems[1].id).toBe('2'); @@ -160,9 +160,9 @@ describe('utils', () => { order: i, })); - const result = getTopNavItems({ config: { items } }); + const result = getAppMenuItems({ config: { items } }); - expect(result.displayedItems).toHaveLength(TOP_NAV_MENU_ITEM_LIMIT); + expect(result.displayedItems).toHaveLength(APP_MENU_ITEM_LIMIT); expect(result.overflowItems).toHaveLength(2); expect(result.shouldOverflow).toBe(true); }); @@ -176,21 +176,21 @@ describe('utils', () => { order: i, })); - const result = getTopNavItems({ + const result = getAppMenuItems({ config: { items, primaryActionItem: { id: 'save', label: 'Save', run: jest.fn(), iconType: 'save' }, }, }); - expect(result.displayedItems).toHaveLength(TOP_NAV_MENU_ITEM_LIMIT - 1); + expect(result.displayedItems).toHaveLength(APP_MENU_ITEM_LIMIT - 1); expect(result.overflowItems).toHaveLength(1); expect(result.shouldOverflow).toBe(true); }); }); - describe('mapTopNavItemToPanelItem', () => { - const baseItem: TopNavMenuPopoverItem = { + describe('mapAppMenuItemToPanelItem', () => { + const baseItem: AppMenuPopoverItem = { id: 'test', label: 'test item', run: jest.fn(), @@ -199,34 +199,34 @@ describe('utils', () => { it('should capitalize label', () => { const item = { ...baseItem, label: 'my label' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.name).toBe('My label'); }); it('should include icon when provided', () => { const item = { ...baseItem, iconType: 'gear' as const }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.icon).toBe('gear'); }); it('should set onClick handler when no href or childPanelId', () => { - const result = mapTopNavItemToPanelItem(baseItem); + const result = mapAppMenuItemToPanelItem(baseItem); expect(result.onClick).toBeDefined(); }); it('should not set onClick when href is provided', () => { const item = { ...baseItem, href: 'http://example.com' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.onClick).toBeUndefined(); expect(result.href).toBe('http://example.com'); }); it('should not set onClick when childPanelId is provided', () => { - const result = mapTopNavItemToPanelItem(baseItem, 1); + const result = mapAppMenuItemToPanelItem(baseItem, 1); expect(result.onClick).toBeUndefined(); expect(result.panel).toBe(1); @@ -234,35 +234,35 @@ describe('utils', () => { it('should set target when href is provided', () => { const item = { ...baseItem, href: 'http://example.com', target: '_blank' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.target).toBe('_blank'); }); it('should not set target when no href', () => { const item = { ...baseItem, target: '_blank' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.target).toBeUndefined(); }); it('should set disabled state', () => { const item = { ...baseItem, disableButton: true }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.disabled).toBe(true); }); it('should include testId as data-test-subj', () => { const item = { ...baseItem, testId: 'my-test-id' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result['data-test-subj']).toBe('my-test-id'); }); it('should include tooltip content and title', () => { const item = { ...baseItem, tooltipContent: 'Content', tooltipTitle: 'Title' }; - const result = mapTopNavItemToPanelItem(item); + const result = mapAppMenuItemToPanelItem(item); expect(result.toolTipContent).toBe('Content'); expect(result.toolTipProps?.title).toBe('Title'); @@ -374,7 +374,7 @@ describe('utils', () => { describe('getPopoverPanels', () => { it('should create single panel for flat items', () => { - const items: TopNavMenuPopoverItem[] = [ + const items: AppMenuPopoverItem[] = [ { id: '1', label: 'Item 1', run: jest.fn(), order: 1 }, { id: '2', label: 'Item 2', run: jest.fn(), order: 2 }, ]; @@ -387,7 +387,7 @@ describe('utils', () => { }); it('should create nested panels for items with sub-items', () => { - const items: TopNavMenuPopoverItem[] = [ + const items: AppMenuPopoverItem[] = [ { id: '1', label: 'Parent', @@ -408,7 +408,7 @@ describe('utils', () => { }); it('should add separator above item when seperator is "above"', () => { - const items: TopNavMenuPopoverItem[] = [ + const items: AppMenuPopoverItem[] = [ { id: '1', label: 'Item 1', run: jest.fn(), order: 1, seperator: 'above' }, ]; @@ -420,7 +420,7 @@ describe('utils', () => { }); it('should add separator below item when seperator is "below"', () => { - const items: TopNavMenuPopoverItem[] = [ + const items: AppMenuPopoverItem[] = [ { id: '1', label: 'Item 1', run: jest.fn(), order: 1, seperator: 'below' }, ]; @@ -432,9 +432,7 @@ describe('utils', () => { }); it('should append action items to main panel when provided', () => { - const items: TopNavMenuPopoverItem[] = [ - { id: '1', label: 'Item 1', run: jest.fn(), order: 1 }, - ]; + const items: AppMenuPopoverItem[] = [{ id: '1', label: 'Item 1', run: jest.fn(), order: 1 }]; const result = getPopoverPanels({ items, @@ -450,9 +448,7 @@ describe('utils', () => { }); it('should use custom startPanelId', () => { - const items: TopNavMenuPopoverItem[] = [ - { id: '1', label: 'Item 1', run: jest.fn(), order: 1 }, - ]; + const items: AppMenuPopoverItem[] = [{ id: '1', label: 'Item 1', run: jest.fn(), order: 1 }]; const result = getPopoverPanels({ items, startPanelId: 10 }); @@ -460,7 +456,7 @@ describe('utils', () => { }); it('should handle deeply nested items', () => { - const items: TopNavMenuPopoverItem[] = [ + const items: AppMenuPopoverItem[] = [ { id: '1', label: 'Level 1', diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.tsx similarity index 83% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.tsx rename to src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.tsx index 078eca5232eb0..0f28486b32b01 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/utils.tsx +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/src/utils.tsx @@ -15,25 +15,25 @@ import { type EuiContextMenuPanelDescriptor, type EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; -import { TopNavMenuPopoverActionButtons } from './top_nav_menu_popover_action_buttons'; +import { AppMenuPopoverActionButtons } from './components/app_menu_popover_action_buttons'; import type { - TopNavMenuConfigBeta, - TopNavMenuItemCommon, - TopNavMenuPopoverItem, - TopNavMenuPrimaryActionItem, - TopNavMenuSecondaryActionItem, + AppMenuConfig, + AppMenuItemCommon, + AppMenuPopoverItem, + AppMenuPrimaryActionItem, + AppMenuSecondaryActionItem, } from './types'; -import { TOP_NAV_MENU_ITEM_LIMIT } from './constants'; +import { APP_MENU_ITEM_LIMIT } from './constants'; /** * Calculate how many items can be displayed based on the presence of action buttons. */ -export const getDisplayedItemsAllowedAmount = (config: TopNavMenuConfigBeta) => { +export const getDisplayedItemsAllowedAmount = (config: AppMenuConfig) => { const actionButtonsAmount = [config.primaryActionItem, config.secondaryActionItem].filter( Boolean ).length; - return TOP_NAV_MENU_ITEM_LIMIT - actionButtonsAmount; + return APP_MENU_ITEM_LIMIT - actionButtonsAmount; }; /** @@ -43,7 +43,7 @@ export const getShouldOverflow = ({ config, displayedItemsAllowedAmount, }: { - config: TopNavMenuConfigBeta; + config: AppMenuConfig; displayedItemsAllowedAmount: number; }) => { if (!config.items) { @@ -56,7 +56,7 @@ export const getShouldOverflow = ({ /** * Split the items into displayed and overflow based on the configuration. */ -export const getTopNavItems = ({ config }: { config: TopNavMenuConfigBeta }) => { +export const getAppMenuItems = ({ config }: { config: AppMenuConfig }) => { if (!config.items) { return { displayedItems: [], @@ -87,15 +87,15 @@ export const getTopNavItems = ({ config }: { config: TopNavMenuConfigBeta }) => }; }; -export const isDisabled = (disableButton: TopNavMenuItemCommon['disableButton']) => +export const isDisabled = (disableButton: AppMenuItemCommon['disableButton']) => Boolean(isFunction(disableButton) ? disableButton() : disableButton); export const getTooltip = ({ tooltipContent, tooltipTitle, }: { - tooltipContent?: TopNavMenuItemCommon['tooltipContent']; - tooltipTitle?: TopNavMenuItemCommon['tooltipTitle']; + tooltipContent?: AppMenuItemCommon['tooltipContent']; + tooltipTitle?: AppMenuItemCommon['tooltipTitle']; }) => { const content = isFunction(tooltipContent) ? tooltipContent() : tooltipContent; const title = isFunction(tooltipTitle) ? tooltipTitle() : tooltipTitle; @@ -106,8 +106,8 @@ export const getTooltip = ({ }; }; -export const mapTopNavItemToPanelItem = ( - item: TopNavMenuPopoverItem, +export const mapAppMenuItemToPanelItem = ( + item: AppMenuPopoverItem, childPanelId?: number ): EuiContextMenuPanelItemDescriptor => { const { content, title } = getTooltip({ @@ -151,16 +151,14 @@ export const getPopoverActionItems = ({ primaryActionItem, secondaryActionItem, }: { - primaryActionItem?: TopNavMenuPrimaryActionItem; - secondaryActionItem?: TopNavMenuSecondaryActionItem; + primaryActionItem?: AppMenuPrimaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; }): EuiContextMenuPanelItemDescriptor[] => { if (!primaryActionItem && !secondaryActionItem) { return []; } - const isHidden = ( - item: TopNavMenuPrimaryActionItem | TopNavMenuSecondaryActionItem | undefined - ) => { + const isHidden = (item: AppMenuPrimaryActionItem | AppMenuSecondaryActionItem | undefined) => { if (!item) return true; const isHiddenInMobile = @@ -184,7 +182,7 @@ export const getPopoverActionItems = ({ { key: 'action-items', renderItem: () => ( - @@ -202,9 +200,9 @@ export const getPopoverPanels = ({ secondaryActionItem, startPanelId = 0, }: { - items: TopNavMenuPopoverItem[]; - primaryActionItem?: TopNavMenuPrimaryActionItem; - secondaryActionItem?: TopNavMenuSecondaryActionItem; + items: AppMenuPopoverItem[]; + primaryActionItem?: AppMenuPrimaryActionItem; + secondaryActionItem?: AppMenuSecondaryActionItem; startPanelId?: number; }): EuiContextMenuPanelDescriptor[] => { const panels: EuiContextMenuPanelDescriptor[] = []; @@ -212,7 +210,7 @@ export const getPopoverPanels = ({ let currentPanelId = startPanelId; const processItems = ( - itemsToProcess: TopNavMenuPopoverItem[], + itemsToProcess: AppMenuPopoverItem[], panelId: number, parentTitle?: string ) => { @@ -228,9 +226,9 @@ export const getPopoverPanels = ({ const childPanelId = currentPanelId; processItems(item.items, childPanelId, item.label); - panelItems.push(mapTopNavItemToPanelItem(item, childPanelId)); + panelItems.push(mapAppMenuItemToPanelItem(item, childPanelId)); } else { - panelItems.push(mapTopNavItemToPanelItem(item)); + panelItems.push(mapAppMenuItemToPanelItem(item)); } if (item.seperator === 'below') { diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/tsconfig.json b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/tsconfig.json new file mode 100644 index 0000000000000..2ac2f49ed823c --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu-components/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@kbn/tsconfig-base/tsconfig.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/split-button", + "@kbn/i18n", + ] +} diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/README.md b/src/core/packages/chrome/app-menu/core-chrome-app-menu/README.md new file mode 100644 index 0000000000000..321e887d042e0 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/README.md @@ -0,0 +1,64 @@ +# App Menu + +`AppMenu` is the replacement for the existing `TopNavMenu` component, providing an improved API and enhanced functionality. + +## Usage + +- Declarative (preferred): + +```tsx +import React, { useEffect } from 'react'; +import { AppMenu } from '@kbn/core-chrome-app-menu'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +interface Props { + config: AppMenuConfig; +} + +const Example = ({ config }: Props) => { + const { chrome } = useKibana().services; + + return ; +}; +``` + +- Imperative: + +```tsx +import React, { useEffect } from 'react'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +interface Props { + config: AppMenuConfig; +} + +const Example = ({ config }: Props) => { + const { chrome } = useKibana().services; + + useEffect(() => { + chrome.setAppMenu(config); + }, [chrome.setAppMenu, config]); + + return
Hello world!
; +}; +``` + +## API changes + +`AppMenu` offers a more restricted API than `TopNavMenu`. + +1. Decoupling from `UnifiedSearch` - top nav menu will no longer be bundled with unified search. You will need to directly import unified search and render it. + +2. Removal of badges - badges will no longer be available in top nav menu. According to UX guidelines, current badges should be moved to use breadcrumbs extension API. + +3. `items` can only be `EuiHeaderLink` (a button with type `text`). For more advanced use cases, use action buttons. + +4. Action buttons - `AppMenu` introduces action buttons: + + - `primaryActionButton` - this is meant to be used for primary actions (e.g saving), can be either an `EuiButton` or a split button, always placed as the rightmost item + + - `secondaryActionButton` - this is meant for secondary actions (e.g adding a new panel), can only be an `EuiButton`, placed to the left from `primaryActionButton` + +5. Removal of `TopNavMenuExtensionsRegistry` - registering global items is no longer possible, add items locally to your application. diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/index.ts b/src/core/packages/chrome/app-menu/core-chrome-app-menu/index.ts new file mode 100644 index 0000000000000..c0a4df78d0197 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { AppMenu } from './src/app_menu'; +export type { AppMenuProps } from './src/app_menu'; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/jest.config.js b/src/core/packages/chrome/app-menu/core-chrome-app-menu/jest.config.js new file mode 100644 index 0000000000000..9f37f61ccfc64 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/jest.config.js @@ -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". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/src/core/packages/chrome/app-menu/core-chrome-app-menu'], +}; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/kibana.jsonc b/src/core/packages/chrome/app-menu/core-chrome-app-menu/kibana.jsonc new file mode 100644 index 0000000000000..4dca33ea5a08e --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-browser", + "id": "@kbn/core-chrome-app-menu", + "owner": "@elastic/appex-sharedux", + "group": "platform", + "visibility": "shared" +} diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/moon.yml b/src/core/packages/chrome/app-menu/core-chrome-app-menu/moon.yml new file mode 100644 index 0000000000000..bb04c33e22a08 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/moon.yml @@ -0,0 +1,46 @@ +# This file is generated by the @kbn/moon package. Any manual edits will be erased! +# To extend this, write your extensions/overrides to 'moon.extend.yml' +# then regenerate this file with: 'node scripts/regenerate_moon_projects.js --update --filter @kbn/core-chrome-app-menu' + +$schema: https://moonrepo.dev/schemas/project.json +id: '@kbn/core-chrome-app-menu' +type: unknown +owners: + defaultOwner: '@elastic/appex-sharedux' +toolchain: + default: node +language: typescript +project: + name: '@kbn/core-chrome-app-menu' + description: Moon project for @kbn/core-chrome-app-menu + channel: '' + owner: '@elastic/appex-sharedux' + metadata: + sourceRoot: src/core/packages/chrome/app-menu/core-chrome-app-menu +dependsOn: + - '@kbn/core-chrome-app-menu-components' +tags: + - shared-browser + - package + - prod + - group-platform + - shared + - jest-unit-tests +fileGroups: + src: + - '**/*.ts' + - '**/*.tsx' + - '!target/**/*' +tasks: + jest: + args: + - '--config' + - $projectRoot/jest.config.js + inputs: + - '@group(src)' + jestCI: + args: + - '--config' + - $projectRoot/jest.config.js + inputs: + - '@group(src)' diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/package.json b/src/core/packages/chrome/app-menu/core-chrome-app-menu/package.json new file mode 100644 index 0000000000000..5c91febf07bb9 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/core-chrome-app-menu", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "sideEffects": false +} diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/src/app_menu.tsx b/src/core/packages/chrome/app-menu/core-chrome-app-menu/src/app_menu.tsx new file mode 100644 index 0000000000000..2cd40ed99c5e9 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/src/app_menu.tsx @@ -0,0 +1,56 @@ +/* + * 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 { useEffect } from 'react'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; + +export interface AppMenuProps { + /** + * The setAppMenu function from ChromeStart. + */ + setAppMenu: (config?: AppMenuConfig) => void; + /** + * The app menu configuration to display in the chrome header. + * When undefined, clears the app menu. + */ + config?: AppMenuConfig; +} + +/** + * A declarative React component for managing the application menu in Kibana's chrome header. + * + * @example + * ```tsx + * import React, { useEffect } from 'react'; + * import { AppMenu } from '@kbn/core-chrome-app-menu'; + * import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; + * import { useKibana } from '@kbn/kibana-react-plugin/public'; + * + *interface Props { + * config: AppMenuConfig; + *} + * + *const Example = ({ config }: Props) => { + * const { chrome } = useKibana().services; + * + * return ; + *}; + *``` + */ +export const AppMenu = ({ setAppMenu, config }: AppMenuProps) => { + useEffect(() => { + setAppMenu(config); + + return () => { + setAppMenu(); + }; + }, [config, setAppMenu]); + + return null; +}; diff --git a/src/core/packages/chrome/app-menu/core-chrome-app-menu/tsconfig.json b/src/core/packages/chrome/app-menu/core-chrome-app-menu/tsconfig.json new file mode 100644 index 0000000000000..400278c7e11d6 --- /dev/null +++ b/src/core/packages/chrome/app-menu/core-chrome-app-menu/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@kbn/tsconfig-base/tsconfig.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core-chrome-app-menu-components" + ] +} diff --git a/src/core/packages/chrome/browser-internal/moon.yml b/src/core/packages/chrome/browser-internal/moon.yml index 822bf61be88e4..fbd01d3c0a6da 100644 --- a/src/core/packages/chrome/browser-internal/moon.yml +++ b/src/core/packages/chrome/browser-internal/moon.yml @@ -66,6 +66,7 @@ dependsOn: - '@kbn/core-feature-flags-browser-mocks' - '@kbn/shared-ux-feedback-snippet' - '@kbn/shared-ux-error-boundary' + - '@kbn/core-chrome-app-menu-components' - '@kbn/shared-ux-label-formatter' tags: - shared-browser diff --git a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx index 3809e78eda002..e778d8a9e5aef 100644 --- a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx +++ b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx @@ -53,6 +53,7 @@ import type { NavigationTreeDefinition, SolutionId, } from '@kbn/core-chrome-browser'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { RecentlyAccessedService } from '@kbn/recently-accessed'; import type { Logger } from '@kbn/logging'; @@ -211,6 +212,7 @@ export class ChromeService { const badge$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(docLinks.links.kibana.askElastic); + const appMenu$ = new BehaviorSubject(undefined); // ChromeStyle is set to undefined by default, which means that no header will be rendered until // setChromeStyle(). This is to avoid a flickering between the "classic" and "project" header meanwhile // we load the user profile to check if the user opted out of the new solution navigation. @@ -276,6 +278,7 @@ export class ChromeService { helpExtension$.next(undefined); breadcrumbs$.next([]); badge$.next(undefined); + appMenu$.next(undefined); docTitle.reset(); }); @@ -395,6 +398,7 @@ export class ChromeService { navControlsRight$={navControls.getRight$()} navControlsExtension$={navControls.getExtension$()} customBranding$={customBranding$} + appMenu$={appMenu$.pipe(takeUntil(this.stop$))} /> ); @@ -455,6 +459,7 @@ export class ChromeService { application={application} globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$} actionMenu$={includeAppMenu ? application.currentActionMenu$ : null} + appMenu$={includeAppMenu ? appMenu$.pipe(takeUntil(this.stop$)) : null} breadcrumbs$={projectNavigation.getProjectBreadcrumbs$().pipe(takeUntil(this.stop$))} breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$))} customBranding$={customBranding$} @@ -563,7 +568,13 @@ export class ChromeService { ); }, getProjectAppMenuComponent: () => { - return ; + return ( + + ); }, // chrome APIs @@ -592,6 +603,12 @@ export class ChromeService { setBreadcrumbs: setClassicBreadcrumbs, + getAppMenu$: () => appMenu$.pipe(takeUntil(this.stop$)), + + setAppMenu: (config?: AppMenuConfig) => { + appMenu$.next(config); + }, + getBreadcrumbsAppendExtensions$: () => breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$)), diff --git a/src/core/packages/chrome/browser-internal/src/ui/header/header.test.tsx b/src/core/packages/chrome/browser-internal/src/ui/header/header.test.tsx index ccdd6176698e0..0adc3cfaa5cac 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/header/header.test.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/header/header.test.tsx @@ -47,6 +47,7 @@ function mockProps() { isLocked$: new BehaviorSubject(false), loadingCount$: new BehaviorSubject(0), isFixed: true, + appMenu$: new BehaviorSubject(undefined), }; } @@ -96,6 +97,7 @@ describe('Header', () => { headerBanner$={headerBanner$} helpMenuLinks$={of([])} isServerless={false} + appMenu$={new BehaviorSubject(undefined)} /> ); expect(component.find('EuiHeader').exists()).toBeTruthy(); diff --git a/src/core/packages/chrome/browser-internal/src/ui/header/header.tsx b/src/core/packages/chrome/browser-internal/src/ui/header/header.tsx index 3777e307792a2..c03f34f4bc5d0 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/header/header.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/header/header.tsx @@ -17,8 +17,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import classnames from 'classnames'; -import React, { createRef, useState } from 'react'; +import React, { createRef, useState, useMemo } from 'react'; import type { Observable } from 'rxjs'; +import { map, EMPTY } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; import type { HttpStart } from '@kbn/core-http-browser'; import type { InternalApplicationStart } from '@kbn/core-application-browser-internal'; import type { @@ -33,8 +35,10 @@ import type { ChromeGlobalHelpExtensionMenuLink, ChromeUserBanner, } from '@kbn/core-chrome-browser'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; import type { CustomBranding } from '@kbn/core-custom-branding-common'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import { HeaderAppMenu } from './header_app_menu'; import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; @@ -74,6 +78,7 @@ export interface HeaderProps { customBranding$: Observable; isServerless: boolean; isFixed: boolean; + appMenu$: Observable; } export function Header({ @@ -94,6 +99,15 @@ export function Header({ const [navId] = useState(htmlIdGenerator()()); const headerActionMenuMounter = useHeaderActionMenuMounter(application.currentActionMenu$); + const hasBeta$ = useMemo( + () => + observables.appMenu$?.pipe( + map((config) => !!config && !!config.items && config.items.length > 0) + ) ?? EMPTY, + [observables.appMenu$] + ); + const hasBetaConfig = useObservable(hasBeta$, false); + const toggleCollapsibleNavRef = createRef void }>(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); @@ -208,7 +222,11 @@ export function Header({ - + {hasBetaConfig ? ( + + ) : ( + + )} diff --git a/src/core/packages/chrome/browser-internal/src/ui/header/header_app_menu.tsx b/src/core/packages/chrome/browser-internal/src/ui/header/header_app_menu.tsx new file mode 100644 index 0000000000000..52ea7f5164fb3 --- /dev/null +++ b/src/core/packages/chrome/browser-internal/src/ui/header/header_app_menu.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 type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; +import React, { lazy, Suspense } from 'react'; +import type { Observable } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; + +const AppMenu = lazy(async () => { + const { AppMenuComponent } = await import('@kbn/core-chrome-app-menu-components'); + return { default: AppMenuComponent }; +}); + +interface Props { + config: Observable; +} + +export const HeaderAppMenu = ({ config }: Props) => { + const menuConfig = useObservable(config, undefined); + + if (menuConfig) { + return ( + + + + ); + } +}; diff --git a/src/core/packages/chrome/browser-internal/src/ui/project/app_menu.tsx b/src/core/packages/chrome/browser-internal/src/ui/project/app_menu.tsx index b532bfcce8a45..52043778bb583 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/project/app_menu.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/project/app_menu.tsx @@ -8,15 +8,20 @@ */ import type { Observable } from 'rxjs'; +import { map, EMPTY } from 'rxjs'; import { useEuiTheme, type UseEuiTheme } from '@elastic/eui'; import type { MountPoint } from '@kbn/core-mount-utils-browser'; import React, { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; +import { HeaderAppMenu } from '../header/header_app_menu'; import { HeaderActionMenu, useHeaderActionMenuMounter } from '../header/header_action_menu'; interface AppMenuBarProps { // TODO: get rid of observable - appMenuActions$: Observable; + appMenuActions$?: Observable | null; + appMenu$: Observable; /** * Whether the menu bar should be fixed (sticky) or static. @@ -55,13 +60,21 @@ const useAppMenuBarStyles = (euiTheme: UseEuiTheme['euiTheme']) => return { root, fixed, static: staticStyle }; }, [euiTheme]); -export const AppMenuBar = ({ appMenuActions$, isFixed = true }: AppMenuBarProps) => { - const headerActionMenuMounter = useHeaderActionMenuMounter(appMenuActions$); +export const AppMenuBar = ({ appMenuActions$, appMenu$, isFixed = true }: AppMenuBarProps) => { + const headerActionMenuMounter = useHeaderActionMenuMounter(appMenuActions$ ?? EMPTY); const { euiTheme } = useEuiTheme(); const styles = useAppMenuBarStyles(euiTheme); - if (!headerActionMenuMounter.mount) return null; + const hasBeta$ = useMemo( + () => + appMenu$?.pipe(map((config) => !!config && !!config.items && config.items.length > 0)) ?? + EMPTY, + [appMenu$] + ); + const hasBetaConfig = useObservable(hasBeta$, false); + + if (!headerActionMenuMounter.mount && !hasBetaConfig) return null; return (
- + {hasBetaConfig ? ( + + ) : ( + + )}
); }; diff --git a/src/core/packages/chrome/browser-internal/src/ui/project/header.tsx b/src/core/packages/chrome/browser-internal/src/ui/project/header.tsx index a4ec069d33605..42092788084ae 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/project/header.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/project/header.tsx @@ -35,9 +35,10 @@ import { i18n } from '@kbn/i18n'; import React, { type ComponentProps, useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import type { Observable } from 'rxjs'; -import { debounceTime } from 'rxjs'; +import { debounceTime, EMPTY } from 'rxjs'; import type { CustomBranding } from '@kbn/core-custom-branding-common'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; import { Breadcrumbs } from './breadcrumbs'; import { HeaderHelpMenu } from '../header/header_help_menu'; import { HeaderNavControls } from '../header/header_nav_controls'; @@ -107,6 +108,7 @@ export interface Props extends Pick, 'isSe breadcrumbs$: Observable; breadcrumbsAppendExtensions$: Observable; actionMenu$?: Observable | null; + appMenu$?: Observable | null; docLinks: DocLinksStart; children: React.ReactNode; customBranding$: Observable; @@ -309,8 +311,12 @@ export const ProjectHeader = ({ - {observables.actionMenu$ && ( - + {(observables.actionMenu$ || observables.appMenu$) && ( + )} ); diff --git a/src/core/packages/chrome/browser-internal/tsconfig.json b/src/core/packages/chrome/browser-internal/tsconfig.json index f4ede1d3a8f3f..ab9ba66709178 100644 --- a/src/core/packages/chrome/browser-internal/tsconfig.json +++ b/src/core/packages/chrome/browser-internal/tsconfig.json @@ -65,6 +65,7 @@ "@kbn/core-feature-flags-browser-mocks", "@kbn/shared-ux-feedback-snippet", "@kbn/shared-ux-error-boundary", + "@kbn/core-chrome-app-menu-components", "@kbn/shared-ux-label-formatter", ], "exclude": [ diff --git a/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts b/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts index 0f01fd92e26d0..4888a7c5294ed 100644 --- a/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts +++ b/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts @@ -96,6 +96,8 @@ const createStartContractMock = () => { }), setGlobalFooter: jest.fn(), getGlobalFooter$: jest.fn().mockReturnValue(new BehaviorSubject(null)), + getAppMenu$: jest.fn().mockReturnValue(new BehaviorSubject(undefined)), + setAppMenu: jest.fn(), }); return startContract; diff --git a/src/core/packages/chrome/browser/moon.yml b/src/core/packages/chrome/browser/moon.yml index dfdf4d5cb5451..1aacbb1fc3004 100644 --- a/src/core/packages/chrome/browser/moon.yml +++ b/src/core/packages/chrome/browser/moon.yml @@ -20,6 +20,7 @@ project: dependsOn: - '@kbn/core-mount-utils-browser' - '@kbn/core-application-common' + - '@kbn/core-chrome-app-menu-components' - '@kbn/deeplinks-devtools' - '@kbn/deeplinks-analytics' - '@kbn/deeplinks-ml' diff --git a/src/core/packages/chrome/browser/src/contracts.ts b/src/core/packages/chrome/browser/src/contracts.ts index dc36e89129cb5..b66faf030239a 100644 --- a/src/core/packages/chrome/browser/src/contracts.ts +++ b/src/core/packages/chrome/browser/src/contracts.ts @@ -9,6 +9,7 @@ import type { ReactNode } from 'react'; import type { Observable } from 'rxjs'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; import type { ChromeNavLink, ChromeNavLinks } from './nav_links'; import type { ChromeRecentlyAccessed } from './recently_accessed'; import type { ChromeDocTitle } from './doc_title'; @@ -96,6 +97,36 @@ export interface ChromeStart { */ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[], params?: ChromeSetBreadcrumbsParams): void; + /** + * Get an observable of the current app menu configuration + */ + getAppMenu$(): Observable; + + /** + * Set the app menu configuration for the current application. + * + * @example + *```tsx + * import React, { useEffect } from 'react'; + * import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; + * import { useKibana } from '@kbn/kibana-react-plugin/public'; + * + * interface Props { + * config: AppMenuConfig; + *} + * + * const Example = ({ config }: Props) => { + * const { chrome } = useKibana().services; + * + * useEffect(() => { + * chrome.setAppMenu(config); + * }, [chrome.setAppMenu, config]); + * + * return
Hello world!
; + * }; + */ + setAppMenu(config?: AppMenuConfig): void; + /** * Get an observable of the current extensions appended to breadcrumbs */ diff --git a/src/core/packages/chrome/browser/tsconfig.json b/src/core/packages/chrome/browser/tsconfig.json index 1e24742e7223b..f0fd8c2ec4603 100644 --- a/src/core/packages/chrome/browser/tsconfig.json +++ b/src/core/packages/chrome/browser/tsconfig.json @@ -14,6 +14,7 @@ "kbn_references": [ "@kbn/core-mount-utils-browser", "@kbn/core-application-common", + "@kbn/core-chrome-app-menu-components", "@kbn/deeplinks-devtools", "@kbn/deeplinks-analytics", "@kbn/deeplinks-ml", diff --git a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx index f0b39af84aac5..d52c0d66988df 100644 --- a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx +++ b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx @@ -9,7 +9,7 @@ import type { ReactNode } from 'react'; import React from 'react'; -import { map } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; import type { ChromeLayoutConfig } from '@kbn/core-chrome-layout-components'; import { ChromeLayout, @@ -85,7 +85,9 @@ export class GridLayout implements LayoutService { // in project style, the project app menu is displayed at the top of application area const projectAppMenu = chrome.getProjectAppMenuComponent(); - const hasAppMenu$ = application.currentActionMenu$.pipe(map((menu) => !!menu)); + const hasAppMenu$ = combineLatest([application.currentActionMenu$, chrome.getAppMenu$()]).pipe( + map(([menu, appMenu]) => !!menu || !!appMenu) + ); const projectSideNavigation = chrome.getProjectSideNavComponentForGridLayout(); diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.stories.tsx b/src/platform/packages/private/shared-ux/storybook/config/app_menu.stories.tsx similarity index 85% rename from src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.stories.tsx rename to src/platform/packages/private/shared-ux/storybook/config/app_menu.stories.tsx index bd05a54494929..f4ae93a54dfe5 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/top_nav_menu_beta.stories.tsx +++ b/src/platform/packages/private/shared-ux/storybook/config/app_menu.stories.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import type { ComponentProps, ReactNode } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { action } from '@storybook/addon-actions'; @@ -15,39 +15,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeader, EuiPageTemplate, useEuiTheme } fr import { UnifiedTabs, useNewTabProps, type TabItem } from '@kbn/unified-tabs'; import { TabStatus, type TabPreviewData } from '@kbn/unified-tabs'; import { css } from '@emotion/react'; -import { TopNavMenuBeta } from './top_nav_menu_beta'; -import type { TopNavMenuConfigBeta } from './types'; +import { AppMenuComponent } from '@kbn/core-chrome-app-menu-components'; +import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components'; -// Hook to replace the tabs menu button icon with arrowDown for Storybook -const useTabsMenuButtonIconOverride = () => { - useEffect(() => { - const arrowDownPath = - 'M1.146 4.646a.5.5 0 01.708 0L8 10.793l6.146-6.147a.5.5 0 01.708.708l-6.5 6.5a.5.5 0 01-.708 0l-6.5-6.5a.5.5 0 010-.708z'; - - const observer = new MutationObserver(() => { - const button = document.querySelector( - '[data-test-subj="unifiedTabs_tabsBarMenuButton"] svg path' - ); - if (button && button.getAttribute('d') !== arrowDownPath) { - button.setAttribute('d', arrowDownPath); - } - }); - - observer.observe(document.body, { childList: true, subtree: true }); - - // Initial check - const button = document.querySelector( - '[data-test-subj="unifiedTabs_tabsBarMenuButton"] svg path' - ); - if (button) { - button.setAttribute('d', arrowDownPath); - } - - return () => observer.disconnect(); - }, []); -}; - -interface TopNavMenuBetaWrapperProps extends ComponentProps { +interface AppMenuWrapperProps extends ComponentProps { showTabs?: boolean; } @@ -56,22 +27,19 @@ const VerticalRule = () => { return ( ); }; -const TopNavMenuBetaWrapper = ({ showTabs = false, ...props }: TopNavMenuBetaWrapperProps) => { +const AppMenuWrapper = ({ showTabs = false, ...props }: AppMenuWrapperProps) => { const { euiTheme } = useEuiTheme(); const { getNewTabDefaultProps } = useNewTabProps({ numberOfInitialItems: 0 }); - // Replace tabs menu button icon with arrowDown - useTabsMenuButtonIconOverride(); - const [tabsState, setTabsState] = useState<{ managedItems: TabItem[]; managedSelectedItemId?: string; @@ -151,11 +119,11 @@ const TopNavMenuBetaWrapper = ({ showTabs = false, ...props }: TopNavMenuBetaWra flex-shrink: 0; `} > - +
) : ( - + ); return ( @@ -179,9 +147,9 @@ const TopNavMenuBetaWrapper = ({ showTabs = false, ...props }: TopNavMenuBetaWra ); }; -const meta: Meta = { - title: 'Navigation/TopNavMenuBeta', - component: TopNavMenuBetaWrapper, +const meta: Meta = { + title: 'Navigation/AppMenu', + component: AppMenuWrapper, argTypes: { showTabs: { control: 'boolean', @@ -201,7 +169,7 @@ const meta: Meta = { parameters: { docs: { description: { - component: 'TopNavMenuBeta is the new design of app menu.', + component: 'AppMenu is the new design of app menu.', }, }, }, @@ -209,9 +177,9 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; -const dashboardEditModeConfig: TopNavMenuConfigBeta = { +const dashboardEditModeConfig: AppMenuConfig = { items: [ { run: action('exit-edit-clicked'), @@ -395,7 +363,7 @@ const dashboardEditModeConfig: TopNavMenuConfigBeta = { }, }; -const discoverConfig: TopNavMenuConfigBeta = { +const discoverConfig: AppMenuConfig = { items: [ { run: action('new-clicked'), diff --git a/src/platform/packages/private/shared-ux/storybook/config/main.ts b/src/platform/packages/private/shared-ux/storybook/config/main.ts index 845769d30268c..a5f1b33762f6b 100644 --- a/src/platform/packages/private/shared-ux/storybook/config/main.ts +++ b/src/platform/packages/private/shared-ux/storybook/config/main.ts @@ -17,7 +17,7 @@ module.exports = { '../../../../shared/shared-ux/**/guide.mdx', '../../../../../../core/packages/chrome/**/*.stories.+(tsx|mdx)', '../../../../shared/kbn-developer-toolbar/**/*.stories.+(tsx|mdx)', - '../../../../../plugins/shared/navigation/**/*.stories.+(tsx|mdx)', + '../app_menu.stories.tsx', ], typescript: { reactDocgen: 'react-docgen-typescript', diff --git a/src/platform/packages/private/shared-ux/storybook/config/moon.yml b/src/platform/packages/private/shared-ux/storybook/config/moon.yml index a76461fcb4105..dff295344b079 100644 --- a/src/platform/packages/private/shared-ux/storybook/config/moon.yml +++ b/src/platform/packages/private/shared-ux/storybook/config/moon.yml @@ -19,6 +19,8 @@ project: sourceRoot: src/platform/packages/private/shared-ux/storybook/config dependsOn: - '@kbn/storybook' + - '@kbn/unified-tabs' + - '@kbn/core-chrome-app-menu-components' tags: - shared-common - package @@ -28,5 +30,6 @@ tags: fileGroups: src: - '**/*.ts' + - '**/*.tsx' - '!target/**/*' tasks: {} diff --git a/src/platform/packages/private/shared-ux/storybook/config/tsconfig.json b/src/platform/packages/private/shared-ux/storybook/config/tsconfig.json index e3b8d879b8aca..d6f59c26f4bbb 100644 --- a/src/platform/packages/private/shared-ux/storybook/config/tsconfig.json +++ b/src/platform/packages/private/shared-ux/storybook/config/tsconfig.json @@ -9,10 +9,13 @@ ] }, "include": [ - "**/*.ts" + "**/*.ts", + "**/*.tsx" ], "kbn_references": [ "@kbn/storybook", + "@kbn/unified-tabs", + "@kbn/core-chrome-app-menu-components", ], "exclude": [ "target/**/*", diff --git a/src/platform/plugins/shared/navigation/moon.yml b/src/platform/plugins/shared/navigation/moon.yml index 2c907e11a4708..594e7ea3c5d06 100644 --- a/src/platform/plugins/shared/navigation/moon.yml +++ b/src/platform/plugins/shared/navigation/moon.yml @@ -35,7 +35,6 @@ dependsOn: - '@kbn/std' - '@kbn/router-utils' - '@kbn/split-button' - - '@kbn/unified-tabs' tags: - plugin - prod diff --git a/src/platform/plugins/shared/navigation/public/README.md b/src/platform/plugins/shared/navigation/public/README.md index 2f6f05b67f151..1ed6911284af5 100644 --- a/src/platform/plugins/shared/navigation/public/README.md +++ b/src/platform/plugins/shared/navigation/public/README.md @@ -13,7 +13,6 @@ interface NavigationPublicStart { ui: { TopNavMenu: (props: TopNavMenuProps) => React.ReactElement; AggregateQueryTopNavMenu: (props: TopNavMenuProps) => React.ReactElement; - TopNavMenuBeta: ({ config, visible}: { config: TopNavMenuConfigBeta, visible: boolean }) => React.ReactElement; createTopNavWithCustomContext: ( customUnifiedSearch?: UnifiedSearchPublicPluginStart, customExtensions?: RegisteredTopNavMenuData[] diff --git a/src/platform/plugins/shared/navigation/public/mocks.tsx b/src/platform/plugins/shared/navigation/public/mocks.tsx index 441103d94ce06..8106727df5c55 100644 --- a/src/platform/plugins/shared/navigation/public/mocks.tsx +++ b/src/platform/plugins/shared/navigation/public/mocks.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { of } from 'rxjs'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { Plugin } from '.'; -import { createTopNav, createTopNavBeta } from './top_nav_menu'; +import { createTopNav } from './top_nav_menu'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; @@ -44,7 +44,6 @@ const createStartContract = (): jest.Mocked => { ui: { TopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])), AggregateQueryTopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])), - TopNavMenuBeta: jest.fn().mockImplementation(createTopNavBeta()), createTopNavWithCustomContext: jest .fn() .mockImplementation(createTopNav(unifiedSearchMock, [])), diff --git a/src/platform/plugins/shared/navigation/public/plugin.tsx b/src/platform/plugins/shared/navigation/public/plugin.tsx index 85642880d0e0b..290d5d62277f8 100644 --- a/src/platform/plugins/shared/navigation/public/plugin.tsx +++ b/src/platform/plugins/shared/navigation/public/plugin.tsx @@ -27,7 +27,7 @@ import type { NavigationPublicStartDependencies, AddSolutionNavigationArg, } from './types'; -import { TopNavMenuExtensionsRegistry, createTopNav, createTopNavBeta } from './top_nav_menu'; +import { TopNavMenuExtensionsRegistry, createTopNav } from './top_nav_menu'; import type { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data'; export class NavigationPublicPlugin @@ -126,7 +126,6 @@ export class NavigationPublicPlugin ui: { TopNavMenu: createTopNav(unifiedSearch, extensions), AggregateQueryTopNavMenu: createTopNav(unifiedSearch, extensions), - TopNavMenuBeta: createTopNavBeta(), createTopNavWithCustomContext: createCustomTopNav, }, addSolutionNavigation: (solutionNavigation) => { diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu/create_top_nav_menu.tsx b/src/platform/plugins/shared/navigation/public/top_nav_menu/create_top_nav_menu.tsx index 65abd2457cfa6..919d42f0fd7e0 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu/create_top_nav_menu.tsx +++ b/src/platform/plugins/shared/navigation/public/top_nav_menu/create_top_nav_menu.tsx @@ -11,7 +11,6 @@ import React, { Suspense, lazy } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import type { AggregateQuery, Query } from '@kbn/es-query'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { TopNavMenuConfigBeta } from '../top_nav_menu_beta'; import type { TopNavMenuProps } from './top_nav_menu'; import type { RegisteredTopNavMenuData } from './top_nav_menu_data'; @@ -20,35 +19,18 @@ const LazyTopNavMenu = lazy(async () => { return { default: TopNavMenu }; }); -const LazyTopNavMenuBeta = lazy(async () => { - const { TopNavMenuBeta } = await import('../top_nav_menu_beta/top_nav_menu_beta'); - return { default: TopNavMenuBeta }; -}); - -export function createTopNavBeta() { - return ({ config, visible }: { config: TopNavMenuConfigBeta; visible?: boolean }) => { - return ( - - - - - - ); - }; -} - /** * @deprecated */ export function createTopNav( /** - * @deprecated TopNavMenuBeta will decouple from UnifiedSearch, so this parameter - * will be removed once TopNavMenuBeta becomes the default. + * @deprecated AppMenu will decouple from UnifiedSearch, so this parameter + * will be removed once AppMenu becomes the default. */ unifiedSearch: UnifiedSearchPublicPluginStart, /** - * @deprecated TopNavMenuBeta will not allow for reigstering global menu items, so this parameter - * will be removed once TopNavMenuBeta becomes the default. + * @deprecated AppMenu will not allow for reigstering global menu items, so this parameter + * will be removed once AppMenu becomes the default. */ extraConfig: RegisteredTopNavMenuData[] ) { diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu/index.ts b/src/platform/plugins/shared/navigation/public/top_nav_menu/index.ts index 2e7a68d47adeb..a5bf155184274 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu/index.ts +++ b/src/platform/plugins/shared/navigation/public/top_nav_menu/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { createTopNav, createTopNavBeta } from './create_top_nav_menu'; +export { createTopNav } from './create_top_nav_menu'; export type { TopNavMenuProps } from './top_nav_menu'; export { TopNavMenu } from './top_nav_menu'; export type { TopNavMenuData } from './top_nav_menu_data'; diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts b/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts index 100da0a1962de..4e5c5e13da8ee 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts +++ b/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_extensions_registry.ts @@ -10,7 +10,7 @@ import type { RegisteredTopNavMenuData } from './top_nav_menu_data'; /** - * @deprecated This registry will be removed once TopNavMenuBeta becomes the default. + * @deprecated This registry will be removed once AppMenu becomes the default. */ export class TopNavMenuExtensionsRegistry { private menuItems: RegisteredTopNavMenuData[]; diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_items.tsx b/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_items.tsx index 0e37ea3274ec0..56b5755bca873 100644 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_items.tsx +++ b/src/platform/plugins/shared/navigation/public/top_nav_menu/top_nav_menu_items.tsx @@ -23,7 +23,7 @@ interface TopNavMenuItemsProps { } /** - * @deprecated Use `TopNavMenuBeta` instead. + * @deprecated Use `AppMenu` instead. */ export const TopNavMenuItems = ({ config, diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/README.md b/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/README.md deleted file mode 100644 index a503d13855cce..0000000000000 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# TopNavMenuBeta - -`TopNavMenuBeta` is the replacement for the existing `TopNavMenu` component, providing an improved API and enhanced functionality for building top navigation menus in your application. - -## Usage - -- Using `core.navigation.ui` API: - -```typescript -// In your plugin's start method -public start(core: CoreStart, plugins: PluginsStart) { - const { ui } = plugins.navigation; - // TODO: Define how to mount in header after upcoming extension point changes. - return { - renderTopNav: (props) => - }; -} -``` - -- Direct import: - -```typescript -import { TopNavMenuBeta} from '@kbn/navigation-plugin/public' - -const MyComponent = (props: TopNavMenuConfigBeta) => { - - // TODO: Define how to mount in header after upcoming extension point changes. - return -} -``` - -## API changes - -`TopNavMenuBeta` offers a more restricted API than `TopNavMenu` - -1. Decoupling from `UnifiedSearch` - top nav menu will no longer be bundled with unified search. You will need to directly import unified search and render it. - -2. Removal of badges - badges will no longer be available in top nav menu. According to UX guidelines, current badges should be moved to use breadcrumbs extension API. - -3. `items` can only be `EuiHeaderLink` (a button with type `text`). For more advanced use cases, use action buttons. - -4. Action buttons - `TopNavMenuBeta` introduces action buttons: - - - `primaryActionButton` - this is meant to be used for primary actions (e.g saving), can be either an `EuiButton` or a split button, always placed as the rightmost item - - - `secondaryActionButton` - this is meant for secondary actions (e.g adding a new panel), can only be an `EuiButton`, placed to the left from `primaryActionButton` - -5. Removal of `TopNavMenuExtensionsRegistry` - registering global items is no longer possible, add items locally to your application. diff --git a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/index.ts b/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/index.ts deleted file mode 100644 index ec24aa4377b29..0000000000000 --- a/src/platform/plugins/shared/navigation/public/top_nav_menu_beta/index.ts +++ /dev/null @@ -1,42 +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". - */ - -export { TopNavMenuBeta } from './top_nav_menu_beta'; -export { TopNavMenuItem } from './top_nav_menu_item'; -export { TopNavMenuActionButton } from './top_nav_menu_action_button'; -export { TopNavMenuOverflowButton } from './top_nav_menu_overflow_button'; -export { TopNavMenuPopover } from './top_nav_menu_popover'; -export { TopNavMenuPopoverActionButtons } from './top_nav_menu_popover_action_buttons'; - -export type { - TopNavMenuConfigBeta, - TopNavMenuItemType, - TopNavMenuSecondaryActionItem, - TopNavMenuPrimaryActionItem, - TopNavMenuPopoverItem, - TopNavMenuSplitButtonProps, -} from './types'; - -export { - TOP_NAV_MENU_ITEM_LIMIT, - TOP_NAV_MENU_NOTIFICATION_INDICATOR_LEFT, - TOP_NAV_MENU_NOTIFICATION_INDICATOR_TOP, -} from './constants'; - -export { - getDisplayedItemsAllowedAmount, - getShouldOverflow, - isDisabled, - getTooltip, - mapTopNavItemToPanelItem, - getTopNavItems, - getPopoverPanels, - getPopoverActionItems, - getIsSelectedColor, -} from './utils'; diff --git a/src/platform/plugins/shared/navigation/public/types.ts b/src/platform/plugins/shared/navigation/public/types.ts index 93ff8139a88da..54cc3109d041b 100644 --- a/src/platform/plugins/shared/navigation/public/types.ts +++ b/src/platform/plugins/shared/navigation/public/types.ts @@ -13,7 +13,6 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { TopNavMenuConfigBeta } from './top_nav_menu_beta'; import type { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, @@ -32,13 +31,6 @@ export interface NavigationPublicStart { ui: { TopNavMenu: (props: TopNavMenuProps) => React.ReactElement; AggregateQueryTopNavMenu: (props: TopNavMenuProps) => React.ReactElement; - TopNavMenuBeta: ({ - config, - visible, - }: { - config: TopNavMenuConfigBeta; - visible?: boolean; - }) => React.ReactElement; createTopNavWithCustomContext: ( customUnifiedSearch?: UnifiedSearchPublicPluginStart, customExtensions?: RegisteredTopNavMenuData[] diff --git a/src/platform/plugins/shared/navigation/tsconfig.json b/src/platform/plugins/shared/navigation/tsconfig.json index 9ea062b546464..6dc30c7caa29e 100644 --- a/src/platform/plugins/shared/navigation/tsconfig.json +++ b/src/platform/plugins/shared/navigation/tsconfig.json @@ -28,7 +28,6 @@ "@kbn/std", "@kbn/router-utils", "@kbn/split-button", - "@kbn/unified-tabs" ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index acf5c6452e2fe..ff4b6f4076e24 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -360,6 +360,10 @@ "@kbn/core-capabilities-server-internal/*": ["src/core/packages/capabilities/server-internal/*"], "@kbn/core-capabilities-server-mocks": ["src/core/packages/capabilities/server-mocks"], "@kbn/core-capabilities-server-mocks/*": ["src/core/packages/capabilities/server-mocks/*"], + "@kbn/core-chrome-app-menu": ["src/core/packages/chrome/app-menu/core-chrome-app-menu"], + "@kbn/core-chrome-app-menu/*": ["src/core/packages/chrome/app-menu/core-chrome-app-menu/*"], + "@kbn/core-chrome-app-menu-components": ["src/core/packages/chrome/app-menu/core-chrome-app-menu-components"], + "@kbn/core-chrome-app-menu-components/*": ["src/core/packages/chrome/app-menu/core-chrome-app-menu-components/*"], "@kbn/core-chrome-browser": ["src/core/packages/chrome/browser"], "@kbn/core-chrome-browser/*": ["src/core/packages/chrome/browser/*"], "@kbn/core-chrome-browser-internal": ["src/core/packages/chrome/browser-internal"], diff --git a/yarn.lock b/yarn.lock index 873f7a749110e..9da8e44649841 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5228,6 +5228,14 @@ version "0.0.0" uid "" +"@kbn/core-chrome-app-menu-components@link:src/core/packages/chrome/app-menu/core-chrome-app-menu-components": + version "0.0.0" + uid "" + +"@kbn/core-chrome-app-menu@link:src/core/packages/chrome/app-menu/core-chrome-app-menu": + version "0.0.0" + uid "" + "@kbn/core-chrome-browser-internal@link:src/core/packages/chrome/browser-internal": version "0.0.0" uid ""