diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx
index cce2e4012f5..893a391da3b 100644
--- a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx
+++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx
@@ -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();
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();
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();
+
+ expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
+ });
+
+ it('should render "Add" extension point menu button in split mode', async () => {
+ renderWithExploreStore();
+
+ 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();
+
+ 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);
diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx
index f360f839a39..0d6508b8acb 100644
--- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx
+++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx
@@ -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 = (
-
- );
+ const menu = ;
return (
<>
diff --git a/public/app/features/explore/extensions/ToolbarExtensionPointMenu.tsx b/public/app/features/explore/extensions/ToolbarExtensionPointMenu.tsx
new file mode 100644
index 00000000000..651cc5103e8
--- /dev/null
+++ b/public/app/features/explore/extensions/ToolbarExtensionPointMenu.tsx
@@ -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 (
+
+ );
+}
+
+function renderItems(extensions: PluginExtensionLink[], onSelect: (link: PluginExtensionLink) => void): JSX.Element[] {
+ return extensions.map((extension) => (
+ {
+ if (extension.path) {
+ return onSelect(extension);
+ }
+ extension.onClick?.(event);
+ }}
+ />
+ ));
+}
+
+type ExtensionLinksResult = {
+ uncategorised: PluginExtensionLink[];
+ categorised: Record;
+};
+
+function useExtensionLinksByCategory(extensions: PluginExtensionLink[]): ExtensionLinksResult {
+ return useMemo(() => {
+ const uncategorised: PluginExtensionLink[] = [];
+ const categorised: Record = {};
+
+ 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]);
+}
diff --git a/public/app/features/explore/extensions/getExploreExtensionConfigs.test.tsx b/public/app/features/explore/extensions/getExploreExtensionConfigs.test.tsx
index 959194fea6d..402f80bdf83 100644
--- a/public/app/features/explore/extensions/getExploreExtensionConfigs.test.tsx
+++ b/public/app/features/explore/extensions/getExploreExtensionConfigs.test.tsx
@@ -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',
},
]);
});
diff --git a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx
index a325e63a6eb..7f95f4e714f 100644
--- a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx
+++ b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx
@@ -14,10 +14,11 @@ export function getExploreExtensionConfigs(): PluginExtensionLinkConfig[] {
try {
return [
createExtensionLinkConfig({
- 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) ||