ExtensionSidebar: Pass closeSidebar to plugin helpers (#108171)

* ExtensionSidebar: Add `CloseExtensionSidebarEvent`

* ExtensionSidebar: Use `CloseExtensionSidebarEvent` in provider

* ExtensionSidebar: Pass `closeSidebar` to plugin helpers
This commit is contained in:
Sven Grossmann
2025-07-16 11:35:15 +00:00
committed by GitHub
parent b232ba5396
commit 4588c6c11c
5 changed files with 86 additions and 15 deletions
@@ -174,6 +174,11 @@ export type PluginExtensionEventHelpers<Context extends object = object> = {
* @param props The props to be passed to the component.
*/
openSidebar: (componentTitle: string, props?: Record<string, unknown>) => void;
/**
* @internal
* Closes the extension sidebar.
*/
closeSidebar: () => void;
};
// Extension Points & Contexts
@@ -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(
<ExtensionSidebarContextProvider>
<TestComponent />
@@ -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 (
<div>
<div data-testid="is-open">{context.isOpen.toString()}</div>
<div data-testid="docked-component-id">{context.dockedComponentId || 'undefined'}</div>
<button onClick={() => context.setDockedComponentId(componentId)}>Open Sidebar</button>
</div>
);
};
render(
<ExtensionSidebarContextProvider>
<TestComponentWithProps />
</ExtensionSidebarContextProvider>
);
// 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(
<ExtensionSidebarContextProvider>
<TestComponent />
@@ -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', () => {
@@ -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(() => {
@@ -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}'`);
+4
View File
@@ -194,6 +194,10 @@ export class OpenExtensionSidebarEvent extends BusEventWithPayload<OpenExtension
static type = 'open-extension-sidebar';
}
export class CloseExtensionSidebarEvent extends BusEventBase {
static type = 'close-extension-sidebar';
}
/**
* @deprecated use ShowModalReactEvent instead that has this capability built in
*/