mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 00:38:58 +08:00
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>
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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';
|
||||
}
|
||||
|
Reference in New Issue
Block a user