Introduce ToggleExtensionSidebar
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
|
||||
|
||||
+158
-4
@@ -3,7 +3,7 @@ import { render, screen, act } from '@testing-library/react';
|
||||
import { store, EventBusSrv, EventBus } 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,
|
||||
@@ -334,8 +334,161 @@ 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 open when receiving ToggleExtensionSidebarEvent and sidebar is closed', () => {
|
||||
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>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
|
||||
|
||||
act(() => {
|
||||
// Find the ToggleExtensionSidebarEvent subscriber
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls.find((call) => call[0] === ToggleExtensionSidebarEvent);
|
||||
expect(toggleEventSubscriberCall).toBeDefined();
|
||||
const [, subscriberFn] = toggleEventSubscriberCall!;
|
||||
|
||||
// Call the toggle event handler
|
||||
subscriberFn(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
props: { testProp: 'test value' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should toggle sidebar closed when receiving ToggleExtensionSidebarEvent for currently open component', () => {
|
||||
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);
|
||||
|
||||
act(() => {
|
||||
// Find the ToggleExtensionSidebarEvent subscriber
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls.find((call) => call[0] === ToggleExtensionSidebarEvent);
|
||||
expect(toggleEventSubscriberCall).toBeDefined();
|
||||
const [, subscriberFn] = toggleEventSubscriberCall!;
|
||||
|
||||
// Call the toggle event handler with the same component
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
act(() => {
|
||||
// Find the ToggleExtensionSidebarEvent subscriber
|
||||
const toggleEventSubscriberCall = subscribeSpy.mock.calls.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 +503,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, type ExtensionInfo } 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';
|
||||
|
||||
@@ -171,13 +171,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(new OpenExtensionSidebarEvent(event.payload));
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { toggleAssistant, isAssistantAvailable } from '@grafana/assistant';
|
||||
import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from '@grafana/data';
|
||||
import { LocationService } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -38,7 +39,8 @@ export class KeybindingSrv {
|
||||
}
|
||||
/** string for VizPanel key and number for panelId */
|
||||
private panelId: string | number | null = null;
|
||||
|
||||
private assistantSubscription: { unsubscribe: () => void } | null = null;
|
||||
|
||||
clearAndInitGlobalBindings(route: RouteDescriptor) {
|
||||
mousetrap.reset();
|
||||
|
||||
@@ -51,6 +53,8 @@ export class KeybindingSrv {
|
||||
this.bind('g e', this.goToExplore);
|
||||
this.bind('g a', this.openAlerting);
|
||||
this.bind('g p', this.goToProfile);
|
||||
// Conditionally bind open Assistant shortcut ('cmd .') if Assistant is available
|
||||
this.bindAssistantShortcutIfAvailable();
|
||||
this.bind('esc', this.exit);
|
||||
this.bindGlobalEsc();
|
||||
}
|
||||
@@ -120,6 +124,30 @@ export class KeybindingSrv {
|
||||
appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
|
||||
}
|
||||
|
||||
private bindAssistantShortcutIfAvailable() {
|
||||
// Clean up any existing subscription
|
||||
if (this.assistantSubscription) {
|
||||
this.assistantSubscription.unsubscribe();
|
||||
}
|
||||
// Subscribe to assistant availability and bind/unbind shortcut accordingly
|
||||
this.assistantSubscription = isAssistantAvailable().subscribe((available) => {
|
||||
if (available) {
|
||||
this.bind('mod+.', this.toggleAssistant);
|
||||
} else {
|
||||
// Unbind the shortcut if assistant becomes unavailable
|
||||
mousetrap.unbind('mod+.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private toggleAssistant() {
|
||||
toggleAssistant({
|
||||
origin: 'grafana/keyboard-shortcut',
|
||||
prompt: '',
|
||||
context: [],
|
||||
});
|
||||
}
|
||||
|
||||
private exit() {
|
||||
const search = this.locationService.getSearchObject();
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -551,6 +556,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