mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
PluginExtensions: Add category support to explore toolbar extension point (#72194)
Added support for categories to explore UI extensions.
This commit is contained in:
parent
43fb14ef8c
commit
7aeca1516a
@ -60,7 +60,8 @@ describe('ToolbarExtensionPoint', () => {
|
||||
pluginId: 'grafana',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Dashboard',
|
||||
title: 'Add to dashboard',
|
||||
category: 'Dashboards',
|
||||
description: 'Add the current query as a panel to a dashboard',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
@ -82,12 +83,13 @@ describe('ToolbarExtensionPoint', () => {
|
||||
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render "Add" extension point menu button in split mode', async () => {
|
||||
it('should render menu with extensions when "Add" is clicked in split mode', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId={'left'} timeZone="browser" splitted={true} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('group', { name: 'Dashboards' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'Add to dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
|
||||
});
|
||||
|
||||
@ -96,7 +98,8 @@ describe('ToolbarExtensionPoint', () => {
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('group', { name: 'Dashboards' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'Add to dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
|
||||
});
|
||||
|
||||
@ -104,7 +107,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" splitted={false} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Dashboard' }));
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Add to dashboard' }));
|
||||
|
||||
const { extensions } = getPluginLinkExtensions({ extensionPointId: PluginExtensionPoints.ExploreToolbarAction });
|
||||
const [extension] = extensions;
|
||||
@ -157,6 +160,59 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with extension points without categories', () => {
|
||||
beforeAll(() => {
|
||||
getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Dashboard',
|
||||
description: 'Add the current query as a panel to a dashboard',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '2',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'ML: Forecast',
|
||||
description: 'Add the query as a ML forecast',
|
||||
path: '/a/grafana-ml-ap/forecast',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should render "Add" extension point menu button', () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" splitted={false} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render "Add" extension point menu button in split mode', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId={'left'} timeZone="browser" splitted={true} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
// Make sure we don't have anything related to categories rendered
|
||||
expect(screen.queryAllByRole('group').length).toBe(0);
|
||||
expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render menu with extensions when "Add" is clicked', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" splitted={false} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
// Make sure we don't have anything related to categories rendered
|
||||
expect(screen.queryAllByRole('group').length).toBe(0);
|
||||
expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('without extension points', () => {
|
||||
beforeAll(() => {
|
||||
contextSrvMock.hasAccess.mockReturnValue(true);
|
||||
|
@ -3,14 +3,14 @@ import React, { lazy, ReactElement, Suspense, useMemo, useState } from 'react';
|
||||
import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange } from '@grafana/data';
|
||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { Dropdown, Menu, ToolbarButton } from '@grafana/ui';
|
||||
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { truncateTitle } from 'app/features/plugins/extensions/utils';
|
||||
import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types';
|
||||
|
||||
import { getExploreItemSelector } from '../state/selectors';
|
||||
|
||||
import { ConfirmNavigationModal } from './ConfirmNavigationModal';
|
||||
import { ToolbarExtensionPointMenu } from './ToolbarExtensionPointMenu';
|
||||
|
||||
const AddToDashboard = lazy(() =>
|
||||
import('./AddToDashboard').then(({ AddToDashboard }) => ({ default: AddToDashboard }))
|
||||
@ -49,24 +49,7 @@ export function ToolbarExtensionPoint(props: Props): ReactElement | null {
|
||||
);
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{extensions.map((extension) => (
|
||||
<Menu.Item
|
||||
ariaLabel={extension.title}
|
||||
icon={extension?.icon || 'plug'}
|
||||
key={extension.id}
|
||||
label={truncateTitle(extension.title, 25)}
|
||||
onClick={(event) => {
|
||||
if (extension.path) {
|
||||
return setSelectedExtension(extension);
|
||||
}
|
||||
extension.onClick?.(event);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
const menu = <ToolbarExtensionPointMenu extensions={extensions} onSelect={setSelectedExtension} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -0,0 +1,76 @@
|
||||
import React, { ReactElement, useMemo } from 'react';
|
||||
|
||||
import { PluginExtensionLink } from '@grafana/data';
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { truncateTitle } from 'app/features/plugins/extensions/utils';
|
||||
|
||||
type Props = {
|
||||
extensions: PluginExtensionLink[];
|
||||
onSelect: (extension: PluginExtensionLink) => void;
|
||||
};
|
||||
|
||||
export function ToolbarExtensionPointMenu({ extensions, onSelect }: Props): ReactElement | null {
|
||||
const { categorised, uncategorised } = useExtensionLinksByCategory(extensions);
|
||||
const showDivider = uncategorised.length > 0 && Object.keys(categorised).length > 0;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<>
|
||||
{Object.keys(categorised).map((category) => (
|
||||
<Menu.Group key={category} label={truncateTitle(category, 25)}>
|
||||
{renderItems(categorised[category], onSelect)}
|
||||
</Menu.Group>
|
||||
))}
|
||||
{showDivider && <Menu.Divider key="divider" />}
|
||||
{renderItems(uncategorised, onSelect)}
|
||||
</>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function renderItems(extensions: PluginExtensionLink[], onSelect: (link: PluginExtensionLink) => void): JSX.Element[] {
|
||||
return extensions.map((extension) => (
|
||||
<Menu.Item
|
||||
ariaLabel={extension.title}
|
||||
icon={extension?.icon || 'plug'}
|
||||
key={extension.id}
|
||||
label={truncateTitle(extension.title, 25)}
|
||||
onClick={(event) => {
|
||||
if (extension.path) {
|
||||
return onSelect(extension);
|
||||
}
|
||||
extension.onClick?.(event);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
type ExtensionLinksResult = {
|
||||
uncategorised: PluginExtensionLink[];
|
||||
categorised: Record<string, PluginExtensionLink[]>;
|
||||
};
|
||||
|
||||
function useExtensionLinksByCategory(extensions: PluginExtensionLink[]): ExtensionLinksResult {
|
||||
return useMemo(() => {
|
||||
const uncategorised: PluginExtensionLink[] = [];
|
||||
const categorised: Record<string, PluginExtensionLink[]> = {};
|
||||
|
||||
for (const link of extensions) {
|
||||
if (!link.category) {
|
||||
uncategorised.push(link);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Array.isArray(categorised[link.category])) {
|
||||
categorised[link.category] = [];
|
||||
}
|
||||
categorised[link.category].push(link);
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
uncategorised,
|
||||
categorised,
|
||||
};
|
||||
}, [extensions]);
|
||||
}
|
@ -15,12 +15,13 @@ describe('getExploreExtensionConfigs', () => {
|
||||
expect(extensions).toEqual([
|
||||
{
|
||||
type: 'link',
|
||||
title: 'Dashboard',
|
||||
title: 'Add to dashboard',
|
||||
description: 'Use the query and panel from explore and create/add it to a dashboard',
|
||||
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||
icon: 'apps',
|
||||
configure: expect.any(Function),
|
||||
onClick: expect.any(Function),
|
||||
category: 'Dashboards',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -14,10 +14,11 @@ export function getExploreExtensionConfigs(): PluginExtensionLinkConfig[] {
|
||||
try {
|
||||
return [
|
||||
createExtensionLinkConfig<PluginExtensionExploreContext>({
|
||||
title: 'Dashboard',
|
||||
title: 'Add to dashboard',
|
||||
description: 'Use the query and panel from explore and create/add it to a dashboard',
|
||||
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||
icon: 'apps',
|
||||
category: 'Dashboards',
|
||||
configure: () => {
|
||||
const canAddPanelToDashboard =
|
||||
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor) ||
|
||||
|
Loading…
Reference in New Issue
Block a user