Dashboards: Fix panel link to Grafana Metrics Drilldown (#103759)

* fix: panel link to Grafana Metrics Drilldown

* test: handling of plugin links
This commit is contained in:
Nick Richmond 2025-04-10 15:50:10 -04:00 committed by GitHub
parent fd6fd91115
commit 73ba19a98e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 187 additions and 13 deletions

View File

@ -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', () => {

View File

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