diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx
index 660aa81e014..b45be60bb2f 100644
--- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx
+++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx
@@ -1,7 +1,7 @@
import { render, screen, act } from '@testing-library/react';
import { store, EventBusSrv, EventBus } from '@grafana/data';
-import { config, getAppEvents, setAppEvents } from '@grafana/runtime';
+import { config, getAppEvents, setAppEvents, locationService } from '@grafana/runtime';
import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { OpenExtensionSidebarEvent } from 'app/types/events';
@@ -13,6 +13,18 @@ import {
EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY,
} from './ExtensionSidebarProvider';
+const mockComponent = {
+ title: 'Test Component',
+ description: 'Test Description',
+ targets: [],
+};
+
+const mockPluginMeta = {
+ pluginId: 'grafana-investigations-app',
+ addedComponents: [mockComponent],
+ addedLinks: [],
+};
+
// Mock the store
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
@@ -38,23 +50,26 @@ jest.mock('@grafana/runtime', () => ({
extensionSidebar: true,
},
},
+ locationService: {
+ getLocation: jest.fn().mockReturnValue({ pathname: '/test-path' }),
+ getLocationObservable: jest.fn(),
+ },
+ usePluginLinks: jest.fn().mockImplementation(() => ({
+ links: [
+ {
+ pluginId: mockPluginMeta.pluginId,
+ title: mockComponent.title,
+ },
+ ],
+ })),
}));
-const mockComponent = {
- title: 'Test Component',
- description: 'Test Description',
- targets: [],
-};
-
-const mockPluginMeta = {
- pluginId: 'grafana-investigations-app',
- addedComponents: [mockComponent],
-};
-
describe('ExtensionSidebarProvider', () => {
let subscribeSpy: jest.SpyInstance;
let originalAppEvents: EventBus;
let mockEventBus: EventBusSrv;
+ let locationObservableMock: { callback: jest.Mock | null; subscribe: jest.Mock };
+ const getExtensionPointPluginMetaMock = jest.mocked(getExtensionPointPluginMeta);
beforeEach(() => {
jest.clearAllMocks();
@@ -66,10 +81,21 @@ describe('ExtensionSidebarProvider', () => {
setAppEvents(mockEventBus);
- (getExtensionPointPluginMeta as jest.Mock).mockReturnValue(new Map([[mockPluginMeta.pluginId, mockPluginMeta]]));
+ getExtensionPointPluginMetaMock.mockReturnValue(new Map([[mockPluginMeta.pluginId, mockPluginMeta]]));
jest.replaceProperty(config.featureToggles, 'extensionSidebar', true);
+ locationObservableMock = {
+ subscribe: jest.fn((callback) => {
+ locationObservableMock.callback = callback;
+ return {
+ unsubscribe: jest.fn(),
+ };
+ }),
+ callback: null,
+ };
+ (locationService.getLocationObservable as jest.Mock).mockReturnValue(locationObservableMock);
+
(store.get as jest.Mock).mockReturnValue(undefined);
(store.set as jest.Mock).mockImplementation(() => {});
(store.delete as jest.Mock).mockImplementation(() => {});
@@ -196,14 +222,16 @@ describe('ExtensionSidebarProvider', () => {
const permittedPluginMeta = {
pluginId: 'grafana-investigations-app',
addedComponents: [mockComponent],
+ addedLinks: [],
};
const prohibitedPluginMeta = {
pluginId: 'disabled-plugin',
addedComponents: [mockComponent],
+ addedLinks: [],
};
- (getExtensionPointPluginMeta as jest.Mock).mockReturnValue(
+ getExtensionPointPluginMetaMock.mockReturnValue(
new Map([
[permittedPluginMeta.pluginId, permittedPluginMeta],
[prohibitedPluginMeta.pluginId, prohibitedPluginMeta],
@@ -328,6 +356,80 @@ describe('ExtensionSidebarProvider', () => {
unmount();
expect(unsubscribeMock).toHaveBeenCalled();
});
+
+ it('should subscribe to location service observable', () => {
+ render(
+
+
+
+ );
+
+ expect(locationService.getLocationObservable).toHaveBeenCalled();
+ expect(locationObservableMock.subscribe).toHaveBeenCalled();
+ });
+
+ it('should update current path when location changes', () => {
+ const usePluginLinksMock = jest.fn().mockReturnValue({ links: [] });
+ jest.requireMock('@grafana/runtime').usePluginLinks = usePluginLinksMock;
+
+ render(
+
+
+
+ );
+
+ expect(usePluginLinksMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({
+ path: '/test-path',
+ }),
+ })
+ );
+
+ act(() => {
+ locationObservableMock.callback?.({ pathname: '/new-path' });
+ });
+
+ expect(usePluginLinksMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({
+ path: '/new-path',
+ }),
+ })
+ );
+ });
+
+ it('should unsubscribe from location service on unmount', () => {
+ const unsubscribeMock = jest.fn();
+ locationObservableMock.subscribe.mockReturnValue({
+ unsubscribe: unsubscribeMock,
+ });
+
+ const { unmount } = render(
+
+
+
+ );
+
+ unmount();
+ expect(unsubscribeMock).toHaveBeenCalled();
+ });
+
+ it('should not include plugins in available components when no links are returned', () => {
+ jest.requireMock('@grafana/runtime').usePluginLinks.mockImplementation(() => ({
+ links: [],
+ }));
+
+ getExtensionPointPluginMetaMock.mockReturnValue(new Map([[mockPluginMeta.pluginId, mockPluginMeta]]));
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('available-components-size')).toHaveTextContent('0');
+ });
});
describe('Utility Functions', () => {
diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx
index 8f64c6a6071..df23d0f2898 100644
--- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx
+++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx
@@ -2,7 +2,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useState
import { useLocalStorage } from 'react-use';
import { store, type ExtensionInfo } from '@grafana/data';
-import { config, getAppEvents } from '@grafana/runtime';
+import { config, getAppEvents, usePluginLinks, locationService } from '@grafana/runtime';
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { OpenExtensionSidebarEvent } from 'app/types/events';
@@ -75,13 +75,43 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY,
DEFAULT_EXTENSION_SIDEBAR_WIDTH
);
+
+ const [currentPath, setCurrentPath] = useState(locationService.getLocation().pathname);
+
+ useEffect(() => {
+ const subscription = locationService.getLocationObservable().subscribe((location) => {
+ setCurrentPath(location.pathname);
+ });
+
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, []);
+
+ // these links are needed to conditionally render the extension component
+ // that means, a plugin would need to register both, a link and a component to
+ // `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control
+ // whether the component is rendered or not
+ const { links } = usePluginLinks({
+ extensionPointId: EXTENSION_SIDEBAR_EXTENSION_POINT_ID,
+ context: {
+ path: currentPath,
+ },
+ });
+
const isEnabled = !!config.featureToggles.extensionSidebar;
// get all components for this extension point, but only for the permitted plugins
// if the extension sidebar is not enabled, we will return an empty map
const availableComponents = isEnabled
? new Map(
- Array.from(getExtensionPointPluginMeta(EXTENSION_SIDEBAR_EXTENSION_POINT_ID).entries()).filter(([pluginId]) =>
- PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId)
+ Array.from(getExtensionPointPluginMeta(EXTENSION_SIDEBAR_EXTENSION_POINT_ID).entries()).filter(
+ ([pluginId, pluginMeta]) =>
+ PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
+ links.some(
+ (link) =>
+ link.pluginId === pluginId &&
+ pluginMeta.addedComponents.some((component) => component.title === link.title)
+ )
)
)
: new Map<
diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx
index b1a6e35ed2c..2b468d9f4c2 100644
--- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx
+++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EventBusSrv, store } from '@grafana/data';
-import { config, setAppEvents } from '@grafana/runtime';
+import { config, setAppEvents, usePluginLinks } from '@grafana/runtime';
import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { ExtensionSidebarContextProvider, useExtensionSidebarContext } from './ExtensionSidebarProvider';
@@ -33,6 +33,15 @@ jest.mock('@grafana/runtime', () => ({
extensionSidebar: true,
},
},
+ usePluginLinks: jest.fn().mockImplementation(() => ({
+ links: [
+ {
+ pluginId: mockPluginMeta.pluginId,
+ title: mockComponent.title,
+ },
+ ],
+ isLoading: false,
+ })),
}));
const mockComponent = {
@@ -121,6 +130,14 @@ describe('ExtensionToolbarItem', () => {
],
};
+ (usePluginLinks as jest.Mock).mockReturnValue({
+ links: [
+ { pluginId: multipleComponentsMeta.pluginId, title: multipleComponentsMeta.addedComponents[0].title },
+ { pluginId: multipleComponentsMeta.pluginId, title: multipleComponentsMeta.addedComponents[1].title },
+ ],
+ isLoading: false,
+ });
+
(getExtensionPointPluginMeta as jest.Mock).mockReturnValue(
new Map([[multipleComponentsMeta.pluginId, multipleComponentsMeta]])
);