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
This commit is contained in:
Eric Hilse
2025-11-24 10:49:41 -07:00
committed by GitHub
parent 43dfad3a0e
commit 335111e783
7 changed files with 85 additions and 12 deletions
@@ -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!
@@ -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(
<MegaMenuHeader handleDockedMenu={handleDockedMenu} handleMegaMenu={handleMegaMenu} onClose={onClose} />
<nav className={styles.content}>
<ScrollContainer height="100%" overflowX="hidden" showScrollIndicators>
<ul className={styles.itemList} aria-label={t('navigation.megamenu.list-label', 'Navigation')}>
{navItems.map((link, index) => (
<MegaMenuItem
key={link.text}
link={link}
isPinned={isPinned}
onClick={state.megaMenuDocked ? undefined : onClose}
activeItem={activeItem}
onPin={onPinItem}
/>
))}
</ul>
<>
<ul className={styles.itemList} aria-label={t('navigation.megamenu.list-label', 'Navigation')}>
{navItems.map((link, index) => (
<MegaMenuItem
key={link.text}
link={link}
isPinned={isPinned}
onClick={state.megaMenuDocked ? undefined : onClose}
activeItem={activeItem}
onPin={onPinItem}
/>
))}
</ul>
<MegaMenuExtensionPoint />
</>
</ScrollContainer>
</nav>
</div>
@@ -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',
});
}
@@ -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 }}
>
<TopBarExtensionPoint />
<TopSearchBarCommandPaletteTrigger />
{unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
{!isSmallScreen && <QuickAdd />}
@@ -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',
});
}
@@ -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', () => ({
+12
View File
@@ -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 });