PluginExtensions: Add category support to explore toolbar extension point (#72194)

Added support for categories to explore UI extensions.
This commit is contained in:
Marcus Andersson 2023-07-24 16:20:36 +02:00 committed by GitHub
parent 43fb14ef8c
commit 7aeca1516a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 27 deletions

View File

@ -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);

View File

@ -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 (
<>

View File

@ -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]);
}

View File

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

View File

@ -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) ||