From 335111e783c59ff37a77640e976957a1052bdf99 Mon Sep 17 00:00:00 2001 From: Eric Hilse Date: Mon, 24 Nov 2025 10:49:41 -0700 Subject: [PATCH] Plugins: Add extension points for grafana-setupguide-app in top nav and menu (#114272) * feat: add plugin extension point to SingleTopBar for setup guide integration * feat: add plugin extension point for grafana-setupguide-app in MegaMenu * feat: add TopBarExtensionPoint for grafana-setupguide-app integration * feat: add MegaMenuExtensionPoint for grafana-setupguide-app integration * feat: simplify MegaMenu and TopBar extension points by removing unused context checks * feat: add MegaMenuAction and SingleTopBarAction to PluginExtensionPoints * refactor: simplify return statement in MegaMenu and TopBar extension points * feat: update extension point IDs in MegaMenu and TopBar components to use PluginExtensionPoints --- .../src/types/pluginExtensions.ts | 2 ++ .../AppChrome/MegaMenu/MegaMenu.tsx | 28 +++++++++++-------- .../MegaMenu/MegaMenuExtensionPoint.tsx | 25 +++++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 2 ++ .../AppChrome/TopBar/TopBarExtensionPoint.tsx | 25 +++++++++++++++++ .../extensions/usePluginComponents.test.tsx | 3 ++ public/test/setupTests.ts | 12 ++++++++ 7 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx create mode 100644 public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 05e7b2b144e..40d20b88c38 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -218,6 +218,8 @@ export enum PluginExtensionPoints { LogsViewResourceAttributes = 'grafana/logsview/resource-attributes', AppChrome = 'grafana/app/chrome/v1', ExtensionSidebar = 'grafana/extension-sidebar/v0-alpha', + MegaMenuAction = 'grafana/megamenu/action', + SingleTopBarAction = 'grafana/singletopbar/action', } // Don't use directly in a plugin! diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 16d100b3bb4..dead4bd810e 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { setBookmark } from 'app/core/reducers/navBarTree'; import { useDispatch, useSelector } from 'app/types/store'; +import { MegaMenuExtensionPoint } from './MegaMenuExtensionPoint'; import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; @@ -110,18 +111,21 @@ export const MegaMenu = memo( diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx new file mode 100644 index 00000000000..96e667ac6ec --- /dev/null +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx @@ -0,0 +1,25 @@ +import { PluginExtensionPoints } from '@grafana/data'; +import { renderLimitedComponents } from '@grafana/runtime'; +import { usePluginComponents } from 'app/features/plugins/extensions/usePluginComponents'; + +/** + * Extension point for plugins to add components to the mega menu. + * Currently restricted to grafana-setupguide-app plugin. + */ +export function MegaMenuExtensionPoint() { + const { components } = usePluginComponents({ + extensionPointId: PluginExtensionPoints.MegaMenuAction, + }); + + // Return null if no components are registered + if (components.length === 0) { + return null; + } + + return renderLimitedComponents({ + props: {}, + components: components, + limit: 1, + pluginId: 'grafana-setupguide-app', + }); +} diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index d61dfdb46de..c68f5572b06 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -28,6 +28,7 @@ import { InviteUserButton } from './InviteUserButton'; import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { SingleTopBarActions } from './SingleTopBarActions'; +import { TopBarExtensionPoint } from './TopBarExtensionPoint'; import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger'; import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight'; @@ -94,6 +95,7 @@ export const SingleTopBar = memo(function SingleTopBar({ data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined} minWidth={{ xs: 'unset', lg: 0 }} > + {unifiedHistoryEnabled && !isSmallScreen && } {!isSmallScreen && } diff --git a/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx b/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx new file mode 100644 index 00000000000..42f81081e30 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx @@ -0,0 +1,25 @@ +import { PluginExtensionPoints } from '@grafana/data'; +import { renderLimitedComponents } from '@grafana/runtime'; +import { usePluginComponents } from 'app/features/plugins/extensions/usePluginComponents'; + +/** + * Extension point for plugins to add components to the top bar. + * Currently restricted to grafana-setupguide-app plugin. + */ +export function TopBarExtensionPoint() { + const { components } = usePluginComponents({ + extensionPointId: PluginExtensionPoints.SingleTopBarAction, + }); + + // Return null if no components are registered + if (components.length === 0) { + return null; + } + + return renderLimitedComponents({ + props: {}, + components: components, + limit: 1, + pluginId: 'grafana-setupguide-app', + }); +} diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index d5740d50b5a..930e2dda359 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -23,6 +23,9 @@ import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponents } from './usePluginComponents'; import { isGrafanaDevMode } from './utils'; +// Unmock usePluginComponents to test the real implementation +jest.unmock('./usePluginComponents'); + jest.mock('./useLoadAppPlugins'); jest.mock('./utils', () => ({ diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index d6c97a0d1c8..2c21f3a8a1e 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -34,6 +34,18 @@ i18next.use(initReactI18next).init({ // the factory uses import.meta.url so we can't use it in CommonJS modules. jest.mock('app/features/dashboard-scene/saving/createDetectChangesWorker.ts'); +// Mock useLoadAppPlugins to prevent async state updates in tests +jest.mock('app/features/plugins/extensions/useLoadAppPlugins', () => ({ + useLoadAppPlugins: jest.fn().mockReturnValue({ isLoading: false }), +})); + +// Mock usePluginComponents to return empty components for all tests by default +// Tests that need to test plugin components can override this mock +jest.mock('app/features/plugins/extensions/usePluginComponents', () => ({ + ...jest.requireActual('app/features/plugins/extensions/usePluginComponents'), + usePluginComponents: jest.fn().mockReturnValue({ components: [], isLoading: false }), +})); + // our tests are heavy in CI due to parallelisation and monaco and kusto // so we increase the default timeout to 2secs to avoid flakiness configure({ asyncUtilTimeout: 2000 });