diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index 6df9186a762..66f2cf2abc3 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -5,9 +5,11 @@ import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; export type GetPluginExtensions = ({ extensionPointId, context, + limitPerPlugin, }: { extensionPointId: string; context?: object | Record; + limitPerPlugin?: number; }) => { extensions: T[]; }; diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 3ed2a9c23a9..9396e9ea1af 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -277,6 +277,7 @@ export function getPanelMenu( const { extensions } = getPluginExtensions({ extensionPointId: PluginExtensionPoints.DashboardPanelMenu, context: createExtensionContext(panel, dashboard), + limitPerPlugin: 2, }); if (extensions.length > 0 && !panel.isEditing) { diff --git a/public/app/features/plugins/extensions/constants.ts b/public/app/features/plugins/extensions/constants.ts deleted file mode 100644 index 9e7dea615f6..00000000000 --- a/public/app/features/plugins/extensions/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const MAX_EXTENSIONS_PER_POINT = 2; diff --git a/public/app/features/plugins/extensions/createPluginExtensionRegistry.test.ts b/public/app/features/plugins/extensions/createPluginExtensionRegistry.test.ts index a8f2c5d045c..571b0f16243 100644 --- a/public/app/features/plugins/extensions/createPluginExtensionRegistry.test.ts +++ b/public/app/features/plugins/extensions/createPluginExtensionRegistry.test.ts @@ -63,33 +63,6 @@ describe('createRegistry()', () => { ); }); - it('should register maximum 2 extensions / plugin / placement', () => { - const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link1, link1] }]); - - expect(Object.getOwnPropertyNames(registry)).toEqual([placement1]); - - // Placement 1 - expect(registry[placement1]).toHaveLength(2); - expect(registry[placement1]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - pluginId, - config: { - ...link1, - configure: expect.any(Function), - }, - }), - expect.objectContaining({ - pluginId, - config: { - ...link1, - configure: expect.any(Function), - }, - }), - ]) - ); - }); - it('should not register link extensions with invalid path configured', () => { const registry = createPluginExtensionRegistry([ { pluginId, extensionConfigs: [{ ...link1, path: 'invalid-path' }, link2] }, diff --git a/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts b/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts index 1004c1957ce..37090c8a736 100644 --- a/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts +++ b/public/app/features/plugins/extensions/createPluginExtensionRegistry.ts @@ -1,14 +1,11 @@ import type { PluginPreloadResult } from '../pluginPreloader'; -import { MAX_EXTENSIONS_PER_POINT } from './constants'; -import { ExtensionsPerPlugin } from './extensionsPerPlugin'; import type { PluginExtensionRegistryItem, PluginExtensionRegistry } from './types'; import { deepFreeze, logWarning } from './utils'; import { isPluginExtensionConfigValid } from './validators'; export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloadResult[]): PluginExtensionRegistry { const registry: PluginExtensionRegistry = {}; - const extensionsPerPlugin = new ExtensionsPerPlugin(); for (const { pluginId, extensionConfigs, error } of pluginPreloadResults) { if (error) { @@ -19,13 +16,6 @@ export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloa for (const extensionConfig of extensionConfigs) { const { extensionPointId } = extensionConfig; - if (!extensionsPerPlugin.allowedToAdd(extensionConfig)) { - logWarning( - `"${pluginId}" plugin has reached the limit of ${MAX_EXTENSIONS_PER_POINT} for "${extensionPointId}", skip registering extension "${extensionConfig.title}".` - ); - continue; - } - if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) { continue; } diff --git a/public/app/features/plugins/extensions/extensionsPerPlugin.ts b/public/app/features/plugins/extensions/extensionsPerPlugin.ts deleted file mode 100644 index 0df5a9d24a1..00000000000 --- a/public/app/features/plugins/extensions/extensionsPerPlugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PluginExtensionConfig } from '@grafana/data'; - -import { MAX_EXTENSIONS_PER_POINT } from './constants'; - -export class ExtensionsPerPlugin { - private extensionsByExtensionPoint: Record = {}; - - allowedToAdd({ extensionPointId, title }: PluginExtensionConfig): boolean { - if (this.countByExtensionPoint(extensionPointId) >= MAX_EXTENSIONS_PER_POINT) { - return false; - } - - this.addExtensionToExtensionPoint(extensionPointId, title); - - return true; - } - - addExtensionToExtensionPoint(extensionPointId: string, extensionTitle: string) { - if (!this.extensionsByExtensionPoint[extensionPointId]) { - this.extensionsByExtensionPoint[extensionPointId] = []; - } - - this.extensionsByExtensionPoint[extensionPointId].push(extensionTitle); - } - - countByExtensionPoint(extensionPointId: string) { - return this.extensionsByExtensionPoint[extensionPointId]?.length ?? 0; - } - - getExtensionTitlesByExtensionPoint(extensionPointId: string) { - return this.extensionsByExtensionPoint[extensionPointId]; - } -} diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.ts b/public/app/features/plugins/extensions/getPluginExtensions.test.ts index 5002fa16c30..32720dffbf9 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.ts @@ -48,6 +48,52 @@ describe('getPluginExtensions()', () => { ); }); + test('should not limit the number of extensions per plugin by default', () => { + // Registering 3 extensions for the same plugin for the same placement + const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link1, link1, link2] }]); + const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 }); + + expect(extensions).toHaveLength(3); + expect(extensions[0]).toEqual( + expect.objectContaining({ + pluginId, + type: PluginExtensionTypes.link, + title: link1.title, + description: link1.description, + path: link1.path, + }) + ); + }); + + test('should be possible to limit the number of extensions per plugin for a given placement', () => { + const registry = createPluginExtensionRegistry([ + { pluginId, extensionConfigs: [link1, link1, link1, link2] }, + { + pluginId: 'my-plugin', + extensionConfigs: [ + { ...link1, path: '/a/my-plugin/declare-incident' }, + { ...link1, path: '/a/my-plugin/declare-incident' }, + { ...link1, path: '/a/my-plugin/declare-incident' }, + { ...link2, path: '/a/my-plugin/declare-incident' }, + ], + }, + ]); + + // Limit to 1 extension per plugin + const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1, limitPerPlugin: 1 }); + + expect(extensions).toHaveLength(2); + expect(extensions[0]).toEqual( + expect.objectContaining({ + pluginId, + type: PluginExtensionTypes.link, + title: link1.title, + description: link1.description, + path: link1.path, + }) + ); + }); + test('should return with an empty list if there are no extensions registered for a placement yet', () => { const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: 'placement-with-no-extensions' }); diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index 443722a8359..4c1c38d3c36 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -26,23 +26,36 @@ import { type GetExtensions = ({ context, extensionPointId, + limitPerPlugin, registry, }: { context?: object | Record; extensionPointId: string; + limitPerPlugin?: number; registry: PluginExtensionRegistry; }) => { extensions: PluginExtension[] }; // Returns with a list of plugin extensions for the given extension point -export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, registry }) => { +export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, limitPerPlugin, registry }) => { const frozenContext = context ? getReadOnlyProxy(context) : {}; const registryItems = registry[extensionPointId] ?? []; // We don't return the extensions separated by type, because in that case it would be much harder to define a sort-order for them. const extensions: PluginExtension[] = []; + const extensionsByPlugin: Record = {}; for (const registryItem of registryItems) { try { const extensionConfig = registryItem.config; + const { pluginId } = registryItem; + + // Only limit if the `limitPerPlugin` is set + if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) { + continue; + } + + if (extensionsByPlugin[pluginId] === undefined) { + extensionsByPlugin[pluginId] = 0; + } // LINK if (isPluginExtensionLinkConfig(extensionConfig)) { @@ -67,6 +80,7 @@ export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, }; extensions.push(extension); + extensionsByPlugin[pluginId] += 1; } // COMPONENT @@ -84,6 +98,7 @@ export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, }; extensions.push(extension); + extensionsByPlugin[pluginId] += 1; } } catch (error) { if (error instanceof Error) {