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 });