mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 05:31: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 { render, screen, act } from '@testing-library/react';
|
||||||
|
|
||||||
import { store, EventBusSrv, EventBus } from '@grafana/data';
|
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 { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||||
import { OpenExtensionSidebarEvent } from 'app/types/events';
|
import { OpenExtensionSidebarEvent } from 'app/types/events';
|
||||||
|
|
||||||
@ -13,6 +13,18 @@ import {
|
|||||||
EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY,
|
EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY,
|
||||||
} from './ExtensionSidebarProvider';
|
} from './ExtensionSidebarProvider';
|
||||||
|
|
||||||
|
const mockComponent = {
|
||||||
|
title: 'Test Component',
|
||||||
|
description: 'Test Description',
|
||||||
|
targets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPluginMeta = {
|
||||||
|
pluginId: 'grafana-investigations-app',
|
||||||
|
addedComponents: [mockComponent],
|
||||||
|
addedLinks: [],
|
||||||
|
};
|
||||||
|
|
||||||
// Mock the store
|
// Mock the store
|
||||||
jest.mock('@grafana/data', () => ({
|
jest.mock('@grafana/data', () => ({
|
||||||
...jest.requireActual('@grafana/data'),
|
...jest.requireActual('@grafana/data'),
|
||||||
@ -38,23 +50,26 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
extensionSidebar: true,
|
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', () => {
|
describe('ExtensionSidebarProvider', () => {
|
||||||
let subscribeSpy: jest.SpyInstance;
|
let subscribeSpy: jest.SpyInstance;
|
||||||
let originalAppEvents: EventBus;
|
let originalAppEvents: EventBus;
|
||||||
let mockEventBus: EventBusSrv;
|
let mockEventBus: EventBusSrv;
|
||||||
|
let locationObservableMock: { callback: jest.Mock | null; subscribe: jest.Mock };
|
||||||
|
const getExtensionPointPluginMetaMock = jest.mocked(getExtensionPointPluginMeta);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@ -66,10 +81,21 @@ describe('ExtensionSidebarProvider', () => {
|
|||||||
|
|
||||||
setAppEvents(mockEventBus);
|
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);
|
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.get as jest.Mock).mockReturnValue(undefined);
|
||||||
(store.set as jest.Mock).mockImplementation(() => {});
|
(store.set as jest.Mock).mockImplementation(() => {});
|
||||||
(store.delete as jest.Mock).mockImplementation(() => {});
|
(store.delete as jest.Mock).mockImplementation(() => {});
|
||||||
@ -196,14 +222,16 @@ describe('ExtensionSidebarProvider', () => {
|
|||||||
const permittedPluginMeta = {
|
const permittedPluginMeta = {
|
||||||
pluginId: 'grafana-investigations-app',
|
pluginId: 'grafana-investigations-app',
|
||||||
addedComponents: [mockComponent],
|
addedComponents: [mockComponent],
|
||||||
|
addedLinks: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const prohibitedPluginMeta = {
|
const prohibitedPluginMeta = {
|
||||||
pluginId: 'disabled-plugin',
|
pluginId: 'disabled-plugin',
|
||||||
addedComponents: [mockComponent],
|
addedComponents: [mockComponent],
|
||||||
|
addedLinks: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
(getExtensionPointPluginMeta as jest.Mock).mockReturnValue(
|
getExtensionPointPluginMetaMock.mockReturnValue(
|
||||||
new Map([
|
new Map([
|
||||||
[permittedPluginMeta.pluginId, permittedPluginMeta],
|
[permittedPluginMeta.pluginId, permittedPluginMeta],
|
||||||
[prohibitedPluginMeta.pluginId, prohibitedPluginMeta],
|
[prohibitedPluginMeta.pluginId, prohibitedPluginMeta],
|
||||||
@ -328,6 +356,80 @@ describe('ExtensionSidebarProvider', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
expect(unsubscribeMock).toHaveBeenCalled();
|
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', () => {
|
describe('Utility Functions', () => {
|
||||||
|
@ -2,7 +2,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useState
|
|||||||
import { useLocalStorage } from 'react-use';
|
import { useLocalStorage } from 'react-use';
|
||||||
|
|
||||||
import { store, type ExtensionInfo } from '@grafana/data';
|
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 { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||||
import { OpenExtensionSidebarEvent } from 'app/types/events';
|
import { OpenExtensionSidebarEvent } from 'app/types/events';
|
||||||
|
|
||||||
@ -75,13 +75,43 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
|||||||
EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY,
|
EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY,
|
||||||
DEFAULT_EXTENSION_SIDEBAR_WIDTH
|
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;
|
const isEnabled = !!config.featureToggles.extensionSidebar;
|
||||||
// get all components for this extension point, but only for the permitted plugins
|
// 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
|
// if the extension sidebar is not enabled, we will return an empty map
|
||||||
const availableComponents = isEnabled
|
const availableComponents = isEnabled
|
||||||
? new Map(
|
? new Map(
|
||||||
Array.from(getExtensionPointPluginMeta(EXTENSION_SIDEBAR_EXTENSION_POINT_ID).entries()).filter(([pluginId]) =>
|
Array.from(getExtensionPointPluginMeta(EXTENSION_SIDEBAR_EXTENSION_POINT_ID).entries()).filter(
|
||||||
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId)
|
([pluginId, pluginMeta]) =>
|
||||||
|
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
|
||||||
|
links.some(
|
||||||
|
(link) =>
|
||||||
|
link.pluginId === pluginId &&
|
||||||
|
pluginMeta.addedComponents.some((component) => component.title === link.title)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: new Map<
|
: new Map<
|
||||||
|
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { EventBusSrv, store } from '@grafana/data';
|
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 { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||||
|
|
||||||
import { ExtensionSidebarContextProvider, useExtensionSidebarContext } from './ExtensionSidebarProvider';
|
import { ExtensionSidebarContextProvider, useExtensionSidebarContext } from './ExtensionSidebarProvider';
|
||||||
@ -33,6 +33,15 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
extensionSidebar: true,
|
extensionSidebar: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
usePluginLinks: jest.fn().mockImplementation(() => ({
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
pluginId: mockPluginMeta.pluginId,
|
||||||
|
title: mockComponent.title,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isLoading: false,
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockComponent = {
|
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(
|
(getExtensionPointPluginMeta as jest.Mock).mockReturnValue(
|
||||||
new Map([[multipleComponentsMeta.pluginId, multipleComponentsMeta]])
|
new Map([[multipleComponentsMeta.pluginId, multipleComponentsMeta]])
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user