diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx index 2b468d9f4c2..45b2c662cc7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx @@ -229,4 +229,41 @@ describe('ExtensionToolbarItem', () => { expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); + + it('should render individual buttons when multiple plugins are available', async () => { + const plugin1Meta = { + pluginId: 'grafana-investigations-app', + addedComponents: [{ ...mockComponent, title: 'Investigations' }], + }; + + const plugin2Meta = { + pluginId: 'grafana-assistant-app', + addedComponents: [{ ...mockComponent, title: 'Assistant' }], + }; + + (usePluginLinks as jest.Mock).mockReturnValue({ + links: [ + { pluginId: plugin1Meta.pluginId, title: plugin1Meta.addedComponents[0].title }, + { pluginId: plugin2Meta.pluginId, title: plugin2Meta.addedComponents[0].title }, + ], + isLoading: false, + }); + + (getExtensionPointPluginMeta as jest.Mock).mockReturnValue( + new Map([ + [plugin1Meta.pluginId, plugin1Meta], + [plugin2Meta.pluginId, plugin2Meta], + ]) + ); + + setup(); + + // Should render two separate buttons, not a dropdown + const buttons = screen.getAllByTestId(/extension-toolbar-button-open/); + expect(buttons).toHaveLength(2); + + // Each button should have the correct title + expect(buttons[0]).toHaveAttribute('aria-label', 'Open Investigations'); + expect(buttons[1]).toHaveAttribute('aria-label', 'Open Assistant'); + }); }); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index 02cad31ea91..50fe56630af 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -1,3 +1,4 @@ +import { ExtensionInfo } from '@grafana/data'; import { Dropdown, Menu } from '@grafana/ui'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; @@ -9,91 +10,75 @@ import { } from './ExtensionSidebarProvider'; import { ExtensionToolbarItemButton } from './ExtensionToolbarItemButton'; -export function ExtensionToolbarItem() { - const { availableComponents, dockedComponentId, setDockedComponentId, isOpen, isEnabled } = - useExtensionSidebarContext(); +type ComponentWithPluginId = ExtensionInfo & { pluginId: string }; - let dockedComponentTitle = ''; - let dockedPluginId = ''; - if (dockedComponentId) { - const dockedComponent = getComponentMetaFromComponentId(dockedComponentId); - if (dockedComponent) { - dockedComponentTitle = dockedComponent.componentTitle; - dockedPluginId = dockedComponent.pluginId; - } - } +export function ExtensionToolbarItem() { + const { availableComponents, dockedComponentId, setDockedComponentId, isEnabled } = useExtensionSidebarContext(); if (!isEnabled || availableComponents.size === 0) { return null; } - // get a flat list of all components with their pluginId - const components = Array.from(availableComponents.entries()).flatMap(([pluginId, { addedComponents }]) => - addedComponents.map((c) => ({ ...c, pluginId })) - ); + const dockedMeta = dockedComponentId ? getComponentMetaFromComponentId(dockedComponentId) : null; - if (components.length === 0) { - return null; - } + const renderPluginButton = (pluginId: string, components: ComponentWithPluginId[]) => { + if (components.length === 1) { + const component = components[0]; + const componentId = getComponentIdFromComponentMeta(pluginId, component); + const isActive = dockedComponentId === componentId; - if (components.length === 1) { - return ( - <> + return ( { - if (isOpen) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); - } - }} - pluginId={components[0].pluginId} + key={pluginId} + isOpen={isActive} + title={component.title} + onClick={() => setDockedComponentId(isActive ? undefined : componentId)} + pluginId={pluginId} /> - - + ); + } + + const isPluginActive = dockedMeta?.pluginId === pluginId; + const MenuItems = ( + + {components.map((c) => { + const id = getComponentIdFromComponentMeta(pluginId, c); + return ( + setDockedComponentId(dockedComponentId === id ? undefined : id)} + /> + ); + })} + ); - } + + return isPluginActive ? ( + setDockedComponentId(undefined)} + pluginId={pluginId} + /> + ) : ( + + + + ); + }; - const MenuItems = ( - - {components.map((c) => { - const id = getComponentIdFromComponentMeta(c.pluginId, c); - return ( - { - if (isOpen && dockedComponentId === id) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(id); - } - }} - /> - ); - })} - - ); return ( <> - {isOpen ? ( - { - if (isOpen) { - setDockedComponentId(undefined); - } - }} - pluginId={dockedPluginId} - /> - ) : ( - - - + {/* renders a single `ExtensionToolbarItemButton` for each plugin; if a plugin has multiple components, it renders them inside a `Dropdown` */} + {Array.from(availableComponents.entries()).map( + ([pluginId, { addedComponents }]: [string, { addedComponents: ExtensionInfo[] }]) => + renderPluginButton( + pluginId, + addedComponents.map((c: ExtensionInfo) => ({ ...c, pluginId })) + ) )} diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx index 44e33526095..3dcc53730a7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx @@ -16,6 +16,8 @@ function getPluginIcon(pluginId?: string): string { switch (pluginId) { case 'grafana-grafanadocsplugin-app': return 'book'; + case 'grafana-investigations-app': + return 'eye'; default: return 'ai-sparkle'; }