diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 919ded3d92a..e64bf16cfdb 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -174,6 +174,11 @@ export type PluginExtensionEventHelpers = { * @param props The props to be passed to the component. */ openSidebar: (componentTitle: string, props?: Record) => void; + /** + * @internal + * Closes the extension sidebar. + */ + closeSidebar: () => void; }; // Extension Points & Contexts diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx index b45be60bb2f..61651cabb84 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx @@ -3,7 +3,7 @@ import { render, screen, act } from '@testing-library/react'; import { store, EventBusSrv, EventBus } from '@grafana/data'; import { config, getAppEvents, setAppEvents, locationService } from '@grafana/runtime'; import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; -import { OpenExtensionSidebarEvent } from 'app/types/events'; +import { OpenExtensionSidebarEvent, CloseExtensionSidebarEvent } from 'app/types/events'; import { ExtensionSidebarContextProvider, @@ -249,7 +249,7 @@ describe('ExtensionSidebarProvider', () => { expect(screen.getByTestId('plugin-ids')).toHaveTextContent(permittedPluginMeta.pluginId); }); - it('should subscribe to OpenExtensionSidebarEvent when feature is enabled', async () => { + it('should subscribe to OpenExtensionSidebarEvent and CloseExtensionSidebarEvent when feature is enabled', async () => { render( @@ -257,9 +257,10 @@ describe('ExtensionSidebarProvider', () => { ); expect(subscribeSpy).toHaveBeenCalledWith(OpenExtensionSidebarEvent, expect.any(Function)); + expect(subscribeSpy).toHaveBeenCalledWith(CloseExtensionSidebarEvent, expect.any(Function)); }); - it('should not subscribe to OpenExtensionSidebarEvent when feature is disabled', () => { + it('should not subscribe to OpenExtensionSidebarEvent or CloseExtensionSidebarEvent when feature is disabled', () => { jest.replaceProperty(config.featureToggles, 'extensionSidebar', false); render( @@ -341,12 +342,58 @@ describe('ExtensionSidebarProvider', () => { expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); - it('should unsubscribe from OpenExtensionSidebarEvent on unmount', () => { - const unsubscribeMock = jest.fn(); - subscribeSpy.mockReturnValue({ - unsubscribe: unsubscribeMock, + it('should close sidebar when receiving a CloseExtensionSidebarEvent', () => { + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + + const TestComponentWithProps = () => { + const context = useExtensionSidebarContext(); + return ( +
+
{context.isOpen.toString()}
+
{context.dockedComponentId || 'undefined'}
+ +
+ ); + }; + + render( + + + + ); + + // First open the sidebar manually + act(() => { + screen.getByText('Open Sidebar').click(); }); + expect(screen.getByTestId('is-open')).toHaveTextContent('true'); + expect(screen.getByTestId('docked-component-id')).toHaveTextContent(componentId); + + // Now test the close event + act(() => { + // Find the CloseExtensionSidebarEvent subscriber + const closeEventSubscriberCall = subscribeSpy.mock.calls.find((call) => call[0] === CloseExtensionSidebarEvent); + + expect(closeEventSubscriberCall).toBeDefined(); + const [, subscriberFn] = closeEventSubscriberCall!; + + // Call the close event handler + subscriberFn(new CloseExtensionSidebarEvent()); + }); + + expect(screen.getByTestId('is-open')).toHaveTextContent('false'); + expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined'); + }); + + it('should unsubscribe from both OpenExtensionSidebarEvent and CloseExtensionSidebarEvent on unmount', () => { + const unsubscribeMocks = [jest.fn(), jest.fn()]; + let callIndex = 0; + + subscribeSpy.mockImplementation(() => ({ + unsubscribe: unsubscribeMocks[callIndex++], + })); + const { unmount } = render( @@ -354,7 +401,10 @@ describe('ExtensionSidebarProvider', () => { ); unmount(); - expect(unsubscribeMock).toHaveBeenCalled(); + + // Both event subscriptions should be unsubscribed + expect(unsubscribeMocks[0]).toHaveBeenCalled(); + expect(unsubscribeMocks[1]).toHaveBeenCalled(); }); it('should subscribe to location service observable', () => { diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index e1618f113cb..84484eab290 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -4,7 +4,7 @@ import { useLocalStorage } from 'react-use'; import { PluginExtensionPoints, store, type ExtensionInfo } from '@grafana/data'; import { config, getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime'; import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; -import { OpenExtensionSidebarEvent } from 'app/types/events'; +import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent } from 'app/types/events'; import { DEFAULT_EXTENSION_SIDEBAR_WIDTH } from './ExtensionSidebar'; @@ -173,7 +173,10 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo if ( event.payload.pluginId && event.payload.componentTitle && - PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(event.payload.pluginId) + PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(event.payload.pluginId) && + availableComponents + .get(event.payload.pluginId) + ?.addedComponents.some((component) => component.title === event.payload.componentTitle) ) { setDockedComponentWithProps( JSON.stringify({ pluginId: event.payload.pluginId, componentTitle: event.payload.componentTitle }), @@ -182,11 +185,17 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo } }; - const subscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler); - return () => { - subscription.unsubscribe(); + const closeSidebarHandler = () => { + setDockedComponentId(undefined); }; - }, [isEnabled, setDockedComponentWithProps]); + + const openSubscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler); + const closeSubscription = getAppEvents().subscribe(CloseExtensionSidebarEvent, closeSidebarHandler); + return () => { + openSubscription.unsubscribe(); + closeSubscription.unsubscribe(); + }; + }, [isEnabled, setDockedComponentWithProps, availableComponents]); // update the stored docked component id when it changes useEffect(() => { diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index db17481a165..f083059cece 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -20,7 +20,7 @@ import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; +import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; import { ExtensionErrorBoundary } from './ExtensionErrorBoundary'; import { ExtensionsLog, log as baseLog } from './logs/log'; @@ -541,6 +541,9 @@ export function getLinkExtensionOnClick( }) ); }, + closeSidebar: () => { + appEvents.publish(new CloseExtensionSidebarEvent()); + }, }; log.debug(`onClick '${config.title}' at '${extensionPointId}'`); diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 4ec85c07e64..d0a3d7a9873 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -194,6 +194,10 @@ export class OpenExtensionSidebarEvent extends BusEventWithPayload