;
/**
* 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;
`}
>
-
+