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:
@@ -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', () => ({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user