mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 11:02:49 +08:00
Extension Sidebar: Enable conditional rendering of component (#104177)
* Extension Sidebar: Add `links` extension point to conditional render component * Extension Sidebar: Add tests * Extension Sidebar: Fix tests
This commit is contained in:
@ -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(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponent />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponent />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponent />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponent />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('available-components-size')).toHaveTextContent('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
|
@ -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<
|
||||
|
@ -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]])
|
||||
);
|
||||
|
Reference in New Issue
Block a user