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