UI extensions: Refactor the registry and remove the "command" type (#65327)

* Wip

* Wip

* Wip

* Wip

* Wip
This commit is contained in:
Levente Balogh
2023-04-03 10:42:15 +02:00
committed by GitHub
parent bde77e4f79
commit 34f3878d26
36 changed files with 1475 additions and 1547 deletions
@@ -1,15 +1,5 @@
import {
PanelMenuItem,
PluginExtension,
PluginExtensionLink,
PluginExtensionTypes,
PluginExtensionPlacements,
} from '@grafana/data';
import {
PluginExtensionPanelContext,
PluginExtensionRegistryItem,
setPluginsExtensionRegistry,
} from '@grafana/runtime';
import { PanelMenuItem, PluginExtensionPanelContext, PluginExtensionTypes } from '@grafana/data';
import { getPluginExtensions } from '@grafana/runtime';
import config from 'app/core/config';
import * as actions from 'app/features/explore/state/main';
import { setStore } from 'app/store/store';
@@ -25,9 +15,16 @@ jest.mock('app/core/services/context_srv', () => ({
},
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
setPluginExtensionGetter: jest.fn(),
getPluginExtensions: jest.fn(),
}));
describe('getPanelMenu()', () => {
beforeEach(() => {
setPluginsExtensionRegistry({});
(getPluginExtensions as jest.Mock).mockRestore();
(getPluginExtensions as jest.Mock).mockReturnValue({ extensions: [] });
});
it('should return the correct panel menu items', () => {
@@ -111,24 +108,24 @@ describe('getPanelMenu()', () => {
describe('when extending panel menu from plugins', () => {
it('should contain menu item from link extension', () => {
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>({
(getPluginExtensions as jest.Mock).mockReturnValue({
extensions: [
{
pluginId: '...',
type: PluginExtensionTypes.link,
title: 'Declare incident',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
}),
},
],
});
const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
const extensionsSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
expect(moreSubMenu).toEqual(
expect(extensionsSubMenu).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'Declare incident',
@@ -139,24 +136,24 @@ describe('getPanelMenu()', () => {
});
it('should truncate menu item title to 25 chars', () => {
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>({
(getPluginExtensions as jest.Mock).mockReturnValue({
extensions: [
{
pluginId: '...',
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
}),
},
],
});
const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
const extensionsSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
expect(moreSubMenu).toEqual(
expect(extensionsSubMenu).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'Declare incident when...',
@@ -166,94 +163,7 @@ describe('getPanelMenu()', () => {
);
});
it('should use extension for panel menu returned by configure function', () => {
const configure: PluginExtensionRegistryItem<PluginExtensionLink> = () => ({
title: 'Wohoo',
type: PluginExtensionTypes.link,
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
});
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>(
{
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
},
configure
),
],
});
const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
expect(moreSubMenu).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: 'Wohoo',
href: '/a/grafana-basic-app/declare-incident',
}),
])
);
});
it('should hide menu item if configure function returns undefined', () => {
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>(
{
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
},
() => undefined
),
],
});
const panel = new PanelModel({});
const dashboard = createDashboardModelFixture({});
const menuItems = getPanelMenu(dashboard, panel);
const moreSubMenu = menuItems.find((i) => i.text === 'Extensions')?.subMenu;
expect(moreSubMenu).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
text: 'Declare incident when...',
href: '/a/grafana-basic-app/declare-incident',
}),
])
);
});
it('should pass context with correct values when configuring extension', () => {
const configure = jest.fn();
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>(
{
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
},
configure
),
],
});
const panel = new PanelModel({
type: 'timeseries',
id: 1,
@@ -303,65 +213,7 @@ describe('getPanelMenu()', () => {
},
};
expect(configure).toBeCalledWith(context);
});
it('should pass context that can not be edited in configure function', () => {
const configure: PluginExtensionRegistryItem<PluginExtensionLink> = (context) => {
// trying to change values in the context
// @ts-ignore
context.pluginId = 'changed';
return {
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
};
};
setPluginsExtensionRegistry({
[PluginExtensionPlacements.DashboardPanelMenu]: [
createRegistryItem<PluginExtensionLink>(
{
type: PluginExtensionTypes.link,
title: 'Declare incident when pressing this amazing menu item',
description: 'Declaring an incident in the app',
path: '/a/grafana-basic-app/declare-incident',
key: 1,
},
configure
),
],
});
const panel = new PanelModel({
type: 'timeseries',
id: 1,
title: 'My panel',
targets: [
{
refId: 'A',
datasource: {
type: 'testdata',
},
},
],
});
const dashboard = createDashboardModelFixture({
timezone: 'utc',
time: {
from: 'now-5m',
to: 'now',
},
tags: ['database', 'panel'],
uid: '123',
title: 'My dashboard',
});
expect(() => getPanelMenu(dashboard, panel)).toThrowError(TypeError);
expect(getPluginExtensions).toBeCalledWith(expect.objectContaining({ context }));
});
});
@@ -479,11 +331,3 @@ describe('getPanelMenu()', () => {
});
});
});
function createRegistryItem<T extends PluginExtension, C extends object = object>(
extension: T,
configure?: PluginExtensionRegistryItem<T, C>
): PluginExtensionRegistryItem<T, C> {
const defaultConfigure = () => extension;
return configure || defaultConfigure;
}
@@ -1,16 +1,11 @@
import { PanelMenuItem, PluginExtensionPlacements, type PluginExtensionPanelContext } from '@grafana/data';
import {
isPluginExtensionCommand,
isPluginExtensionLink,
PanelMenuItem,
PluginExtensionPlacements,
} from '@grafana/data';
import {
AngularComponent,
getDataSourceSrv,
getPluginExtensions,
locationService,
reportInteraction,
PluginExtensionPanelContext,
} from '@grafana/runtime';
import { PanelCtrl } from 'app/angular/panel/panel_ctrl';
import config from 'app/core/config';
@@ -299,14 +294,6 @@ export function getPanelMenu(
});
continue;
}
if (isPluginExtensionCommand(extension)) {
extensionsMenu.push({
text: truncateTitle(extension.title, 25),
onClick: extension.callHandlerWithContext,
});
continue;
}
}
menu.push({
@@ -340,26 +327,20 @@ function truncateTitle(title: string, length: number): string {
}
function createExtensionContext(panel: PanelModel, dashboard: DashboardModel): PluginExtensionPanelContext {
const timeRange = Object.assign({}, dashboard.time);
return Object.freeze({
return {
id: panel.id,
pluginId: panel.type,
title: panel.title,
timeRange: Object.freeze(timeRange),
timeRange: Object.assign({}, dashboard.time),
timeZone: dashboard.timezone,
dashboard: Object.freeze({
dashboard: {
uid: dashboard.uid,
title: dashboard.title,
tags: Object.freeze(Array.from<string>(dashboard.tags)),
}),
targets: Object.freeze(
panel.targets.map((t) =>
Object.freeze({
refId: t.refId,
pluginId: t.datasource?.type ?? 'unknown',
})
)
),
});
tags: Array.from<string>(dashboard.tags),
},
targets: panel.targets.map((t) => ({
refId: t.refId,
pluginId: t.datasource?.type ?? 'unknown',
})),
};
}