mirror of
https://github.com/grafana/grafana.git
synced 2026-08-09 12:48:18 -05:00
ExtensionSidebar: Introduce ToggleExtensionSidebarEvent (#112325)
* ExtensionSidebar: Use appEvents for opening/closing sidebar * Refactor * Fix lint * Introduce ToggleExtensionSidebar * Revert keyboard shortcuts * Revert "Refactor" This reverts commitffd901ed0c. * Revert "ExtensionSidebar: Use appEvents for opening/closing sidebar" This reverts commit8083bec978. * Fix test * Refactor to prevent flakiness * Try again * Improve mocks * simple * trying anything at this point * Flip test order * Revert "Flip test order" This reverts commit2dd33f744e. * Update togglespyes and act * Try async await * Update test * Update solution * Update * Test * Update * Refactor * Uodate comments * Types * Fix type
This commit is contained in:
@@ -181,6 +181,15 @@ export type PluginExtensionEventHelpers<Context extends object = object> = {
|
||||
* Closes the extension sidebar.
|
||||
*/
|
||||
closeSidebar: () => void;
|
||||
/**
|
||||
* @internal
|
||||
* Toggles the extension sidebar with the registered component.
|
||||
* If the sidebar is open with the same component, it will be closed.
|
||||
* If the sidebar is closed or open with a different component, it will be opened with the specified component.
|
||||
* @param componentTitle The title of the component to be toggled in the sidebar.
|
||||
* @param props The props to be passed to the component.
|
||||
*/
|
||||
toggleSidebar: (componentTitle: string, props?: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
// Extension Points & Contexts
|
||||
|
||||
+140
-7
@@ -1,9 +1,9 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
|
||||
import { store, EventBusSrv, EventBus } from '@grafana/data';
|
||||
import { store, EventBusSrv, EventBus, ExtensionInfo } from '@grafana/data';
|
||||
import { getAppEvents, setAppEvents, locationService } from '@grafana/runtime';
|
||||
import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||
import { OpenExtensionSidebarEvent, CloseExtensionSidebarEvent } from 'app/types/events';
|
||||
import { OpenExtensionSidebarEvent, CloseExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
|
||||
|
||||
import {
|
||||
ExtensionSidebarContextProvider,
|
||||
@@ -17,11 +17,17 @@ const mockComponent = {
|
||||
title: 'Test Component',
|
||||
description: 'Test Description',
|
||||
targets: [],
|
||||
};
|
||||
} as ExtensionInfo;
|
||||
|
||||
const mockDifferentComponent = {
|
||||
title: 'Different Component',
|
||||
description: 'Different Description',
|
||||
targets: [],
|
||||
} as ExtensionInfo;
|
||||
|
||||
const mockPluginMeta = {
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [mockComponent],
|
||||
addedComponents: [mockComponent, mockDifferentComponent],
|
||||
addedLinks: [],
|
||||
};
|
||||
|
||||
@@ -54,6 +60,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
title: mockComponent.title,
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -334,8 +341,133 @@ describe('ExtensionSidebarProvider', () => {
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined');
|
||||
});
|
||||
|
||||
it('should unsubscribe from both OpenExtensionSidebarEvent and CloseExtensionSidebarEvent on unmount', () => {
|
||||
const unsubscribeMocks = [jest.fn(), jest.fn()];
|
||||
it('should subscribe to ToggleExtensionSidebarEvent', () => {
|
||||
render(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponent />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
expect(subscribeSpy).toHaveBeenCalledWith(ToggleExtensionSidebarEvent, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should toggle sidebar when receiving ToggleExtensionSidebarEvent', () => {
|
||||
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>
|
||||
<div data-testid="props">{context.props ? JSON.stringify(context.props) : 'undefined'}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<ExtensionSidebarContextProvider>
|
||||
<TestComponentWithProps />
|
||||
</ExtensionSidebarContextProvider>
|
||||
);
|
||||
|
||||
// Sidebar is closed
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
|
||||
|
||||
// Toggle the sidebar to open it
|
||||
act(() => {
|
||||
// Call the toggle event handler
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls.find((call) => call[0] === ToggleExtensionSidebarEvent);
|
||||
expect(toggleEventSubscriberCall).toBeDefined();
|
||||
const [, subscriberFn] = toggleEventSubscriberCall!;
|
||||
|
||||
subscriberFn(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
props: { testProp: 'test value' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Sidebar is open
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
||||
expect(screen.getByTestId('props')).toHaveTextContent('{"testProp":"test value"}');
|
||||
const expectedComponentId = JSON.stringify({
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
});
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent(expectedComponentId);
|
||||
|
||||
// Toggle the sidebar to close it
|
||||
act(() => {
|
||||
// Call the toggle event handler
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((call) => call[0] === ToggleExtensionSidebarEvent);
|
||||
expect(toggleEventSubscriberCall).toBeDefined();
|
||||
const [, subscriberFn] = toggleEventSubscriberCall!;
|
||||
|
||||
subscriberFn(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
pluginId: mockPluginMeta.pluginId,
|
||||
componentTitle: mockComponent.title,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined');
|
||||
});
|
||||
|
||||
it('should toggle to different component when receiving ToggleExtensionSidebarEvent for different component', () => {
|
||||
const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title);
|
||||
(store.get as jest.Mock).mockReturnValue(componentId);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
// Find the ToggleExtensionSidebarEvent subscriber
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((call) => call[0] === ToggleExtensionSidebarEvent);
|
||||
expect(toggleEventSubscriberCall).toBeDefined();
|
||||
const [, subscriberFn] = toggleEventSubscriberCall!;
|
||||
|
||||
// Call the toggle event handler with a different component
|
||||
subscriberFn(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
pluginId: mockPluginMeta.pluginId,
|
||||
componentTitle: 'Different Component',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
||||
const expectedComponentId = JSON.stringify({
|
||||
pluginId: mockPluginMeta.pluginId,
|
||||
componentTitle: 'Different Component',
|
||||
});
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent(expectedComponentId);
|
||||
});
|
||||
|
||||
it('should unsubscribe from all event subscriptions on unmount', () => {
|
||||
const unsubscribeMocks = [jest.fn(), jest.fn(), jest.fn()];
|
||||
let callIndex = 0;
|
||||
|
||||
subscribeSpy.mockImplementation(() => ({
|
||||
@@ -350,9 +482,10 @@ describe('ExtensionSidebarProvider', () => {
|
||||
|
||||
unmount();
|
||||
|
||||
// Both event subscriptions should be unsubscribed
|
||||
// All event subscriptions should be unsubscribed
|
||||
expect(unsubscribeMocks[0]).toHaveBeenCalled();
|
||||
expect(unsubscribeMocks[1]).toHaveBeenCalled();
|
||||
expect(unsubscribeMocks[2]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should subscribe to location service observable', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLocalStorage } from 'react-use';
|
||||
import { PluginExtensionPoints, store } from '@grafana/data';
|
||||
import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
|
||||
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent } from 'app/types/events';
|
||||
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
|
||||
|
||||
import { DEFAULT_EXTENSION_SIDEBAR_WIDTH, MAX_EXTENSION_SIDEBAR_WIDTH } from './ExtensionSidebar';
|
||||
|
||||
@@ -174,13 +174,28 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
||||
setDockedComponentId(undefined);
|
||||
};
|
||||
|
||||
const toggleSidebarHandler = (event: ToggleExtensionSidebarEvent) => {
|
||||
const currentComponentMeta = getComponentMetaFromComponentId(dockedComponentId ?? '');
|
||||
const isCurrentlyOpen =
|
||||
currentComponentMeta?.pluginId === event.payload.pluginId &&
|
||||
currentComponentMeta?.componentTitle === event.payload.componentTitle;
|
||||
|
||||
if (isCurrentlyOpen) {
|
||||
closeSidebarHandler();
|
||||
} else {
|
||||
openSidebarHandler(event);
|
||||
}
|
||||
};
|
||||
|
||||
const openSubscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler);
|
||||
const closeSubscription = getAppEvents().subscribe(CloseExtensionSidebarEvent, closeSidebarHandler);
|
||||
const toggleSubscription = getAppEvents().subscribe(ToggleExtensionSidebarEvent, toggleSidebarHandler);
|
||||
return () => {
|
||||
openSubscription.unsubscribe();
|
||||
closeSubscription.unsubscribe();
|
||||
toggleSubscription.unsubscribe();
|
||||
};
|
||||
}, [setDockedComponentWithProps, availableComponents]);
|
||||
}, [setDockedComponentWithProps, availableComponents, dockedComponentId]);
|
||||
|
||||
// update the stored docked component id when it changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -20,7 +20,12 @@ 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 { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events';
|
||||
import {
|
||||
CloseExtensionSidebarEvent,
|
||||
OpenExtensionSidebarEvent,
|
||||
ShowModalReactEvent,
|
||||
ToggleExtensionSidebarEvent,
|
||||
} from 'app/types/events';
|
||||
|
||||
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
|
||||
|
||||
@@ -550,6 +555,15 @@ export function getLinkExtensionOnClick(
|
||||
closeSidebar: () => {
|
||||
appEvents.publish(new CloseExtensionSidebarEvent());
|
||||
},
|
||||
toggleSidebar: (componentTitle, context) => {
|
||||
appEvents.publish(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
props: context,
|
||||
pluginId,
|
||||
componentTitle,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
log.debug(`onClick '${config.title}' at '${extensionPointId}'`);
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface OpenExtensionSidebarPayload {
|
||||
componentTitle: string;
|
||||
}
|
||||
|
||||
export interface ToggleExtensionSidebarPayload {
|
||||
props?: Record<string, unknown>;
|
||||
pluginId: string;
|
||||
componentTitle: string;
|
||||
}
|
||||
|
||||
export interface ShowConfirmModalPayload {
|
||||
title?: string;
|
||||
text?: string;
|
||||
@@ -181,6 +187,10 @@ export class CloseExtensionSidebarEvent extends BusEventBase {
|
||||
static type = 'close-extension-sidebar';
|
||||
}
|
||||
|
||||
export class ToggleExtensionSidebarEvent extends BusEventWithPayload<ToggleExtensionSidebarPayload> {
|
||||
static type = 'toggle-extension-sidebar';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use ShowModalReactEvent instead that has this capability built in
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user