ExtensionSidebar: Render multiple sidebar buttons in topnav (#107875)

* feat: modified toolbar item so buttons render invidually

* added icon for investigations

* Update public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

---------

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
pull/107946/head
Jay Clifford 2 weeks ago committed by GitHub
parent b6c4788c2a
commit fccda2440e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 37
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx
  2. 127
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx
  3. 2
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.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');
});
});

@ -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 (
<ExtensionToolbarItemButton
isOpen={isOpen}
title={components[0].title}
onClick={() => {
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}
/>
<NavToolbarSeparator />
</>
);
}
const isPluginActive = dockedMeta?.pluginId === pluginId;
const MenuItems = (
<Menu>
{components.map((c) => {
const id = getComponentIdFromComponentMeta(pluginId, c);
return (
<Menu.Item
key={id}
active={dockedComponentId === id}
label={c.title}
onClick={() => setDockedComponentId(dockedComponentId === id ? undefined : id)}
/>
);
})}
</Menu>
);
}
const MenuItems = (
<Menu>
{components.map((c) => {
const id = getComponentIdFromComponentMeta(c.pluginId, c);
return (
<Menu.Item
key={id}
active={dockedComponentId === id}
label={c.title}
onClick={() => {
if (isOpen && dockedComponentId === id) {
setDockedComponentId(undefined);
} else {
setDockedComponentId(id);
}
}}
/>
);
})}
</Menu>
);
return isPluginActive ? (
<ExtensionToolbarItemButton
key={pluginId}
isOpen
title={dockedMeta?.componentTitle}
onClick={() => setDockedComponentId(undefined)}
pluginId={pluginId}
/>
) : (
<Dropdown key={pluginId} overlay={MenuItems} placement="bottom-end">
<ExtensionToolbarItemButton isOpen={false} pluginId={pluginId} />
</Dropdown>
);
};
return (
<>
{isOpen ? (
<ExtensionToolbarItemButton
isOpen
title={dockedComponentTitle}
onClick={() => {
if (isOpen) {
setDockedComponentId(undefined);
}
}}
pluginId={dockedPluginId}
/>
) : (
<Dropdown overlay={MenuItems} placement="bottom-end">
<ExtensionToolbarItemButton isOpen={false} />
</Dropdown>
{/* 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 }))
)
)}
<NavToolbarSeparator />
</>

@ -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';
}

Loading…
Cancel
Save