From 73ba19a98e259d73f080d4ce8c7c5b89d1e9e5da Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:50:10 -0400 Subject: [PATCH] Dashboards: Fix panel link to Grafana Metrics Drilldown (#103759) * fix: panel link to Grafana Metrics Drilldown * test: handling of plugin links --- .../scene/PanelMenuBehavior.test.tsx | 149 ++++++++++++++++++ .../scene/PanelMenuBehavior.tsx | 51 ++++-- 2 files changed, 187 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 5f0e1ae0e18..998541edf80 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -553,6 +553,155 @@ describe('panelMenuBehavior', () => { expect(menu.state.items?.length).toBe(1); expect(menu.state.items?.[0].text).toBe('Explore'); }); + + describe('plugin links', () => { + it('should not show Metrics Drilldown menu when no Metrics Drilldown links exist', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Other Extension', + description: 'Some other extension', + path: '/a/other-app/action', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeUndefined(); + expect(extensionsMenu).toBeDefined(); + expect(extensionsMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'Other Extension', + href: '/a/other-app/action', + }), + ]); + }); + + it('should separate Metrics Drilldown links into their own menu', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Open in Metrics Drilldown', + description: 'Open current query in Metrics Drilldown', + path: '/a/grafana-metricsdrilldown-app/trail', + category: 'metrics-drilldown', + }, + { + id: '2', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Other Extension', + description: 'Some other extension', + path: '/a/other-app/action', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(menu.state.items?.length).toBe(8); // 6 base items + 2 extension menus + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeDefined(); + expect(metricsDrilldownMenu?.iconClassName).toBe('code-branch'); + expect(metricsDrilldownMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'metrics-drilldown', + type: 'group', + subMenu: expect.arrayContaining([ + expect.objectContaining({ + text: 'Open in Metrics Drilld...', + href: '/a/grafana-metricsdrilldown-app/trail', + }), + ]), + }), + ]); + + expect(extensionsMenu).toBeDefined(); + expect(extensionsMenu?.iconClassName).toBe('plug'); + expect(extensionsMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'Other Extension', + href: '/a/other-app/action', + }), + ]); + }); + + it('should not show extensions menu when no non-Metrics Drilldown links exist', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Open in Metrics Drilldown', + description: 'Open current query in Metrics Drilldown', + path: '/a/grafana-metricsdrilldown-app/trail', + category: 'metrics-drilldown', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeDefined(); + expect(extensionsMenu).toBeUndefined(); + expect(metricsDrilldownMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'metrics-drilldown', + type: 'group', + subMenu: expect.arrayContaining([ + expect.objectContaining({ + text: 'Open in Metrics Drilld...', + href: '/a/grafana-metricsdrilldown-app/trail', + }), + ]), + }), + ]); + }); + }); }); describe('onCreateAlert', () => { diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 82909f80268..664526928a9 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -4,6 +4,7 @@ import { LinkModel, PanelMenuItem, PanelPlugin, + PluginExtensionLink, PluginExtensionPanelContext, PluginExtensionPoints, PluginExtensionTypes, @@ -27,7 +28,6 @@ import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/ge import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup'; import { GetPluginExtensions } from 'app/features/plugins/extensions/types'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; -import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; import { dispatch } from 'app/store/store'; import { AccessControlAction } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -55,6 +55,9 @@ function setupGetPluginExtensions() { return getPluginExtensions; } +// Define the category for metrics drilldown links +const METRICS_DRILLDOWN_CATEGORY = 'metrics-drilldown'; + /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). */ @@ -292,10 +295,6 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - if (config.featureToggles.exploreMetrics) { - await addDataTrailPanelAction(dashboard, panel, items); - } - if (exploreMenuItem) { items.push(exploreMenuItem); } @@ -310,15 +309,41 @@ export function panelMenuBehavior(menu: VizPanelMenu) { limitPerPlugin: 3, }); - const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link); - if (extensions.length > 0 && !dashboard.state.isEditing) { - items.push({ - text: 'Extensions', - iconClassName: 'plug', - type: 'submenu', - subMenu: createExtensionSubMenu(linkExtensions), - }); + const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link); + + // Separate metrics drilldown links from other links + const [metricsDrilldownLinks, otherLinks] = linkExtensions.reduce<[PluginExtensionLink[], PluginExtensionLink[]]>( + ([metricsDrilldownLinks, otherLinks], link) => { + if (link.category === METRICS_DRILLDOWN_CATEGORY) { + metricsDrilldownLinks.push(link); + } else { + otherLinks.push(link); + } + return [metricsDrilldownLinks, otherLinks]; + }, + [[], []] + ); + + // Add specific "Metrics drilldown" menu + if (metricsDrilldownLinks.length > 0) { + items.push({ + text: 'Metrics drilldown', + iconClassName: 'code-branch', + type: 'submenu', + subMenu: createExtensionSubMenu(metricsDrilldownLinks), + }); + } + + // Add generic "Extensions" menu for other links + if (otherLinks.length > 0) { + items.push({ + text: 'Extensions', + iconClassName: 'plug', + type: 'submenu', + subMenu: createExtensionSubMenu(otherLinks), + }); + } } if (moreSubMenu.length) {