diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index f40955c3f54..255a8a6161c 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -1,7 +1,13 @@ import { act, render, renderHook, screen } from '@testing-library/react'; import React from 'react'; -import { PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data'; +import { + PluginContextProvider, + PluginExtensionPoints, + PluginLoadingStrategy, + PluginMeta, + PluginType, +} from '@grafana/data'; import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; @@ -105,6 +111,32 @@ describe('usePluginComponents()', () => { }, }; + config.apps[pluginId] = { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + extensions: { + addedLinks: [], + addedComponents: [], + addedFunctions: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -459,6 +491,49 @@ describe('usePluginComponents()', () => { expect(log.error).not.toHaveBeenCalled(); }); + // It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points. + it('should not validate the extension point meta-info for core plugins', () => { + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + const componentConfig = { + targets: extensionPointId, + title: '1', + description: '1', + component: () =>
Component
, + }; + + // The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps). + config.apps[pluginId].extensions.addedComponents = [componentConfig]; + + wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + registries.addedComponentsRegistry.register({ + pluginId, + configs: [componentConfig], + }); + + // Trying to render an extension point that is not defined in the plugin meta + // (No restrictions due to being a core plugin) + let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); + expect(result.current.components.length).toBe(1); + expect(log.error).not.toHaveBeenCalled(); + }); + it('should not validate the extension point id in production mode', () => { // Empty list of extension points in the plugin meta (from plugin.json) wrapper = ({ children }: { children: React.ReactNode }) => ( diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index 6f37f3bdbba..d07a2e5ac07 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -29,6 +29,7 @@ export function usePluginComponents({ return useMemo(() => { const isInsidePlugin = Boolean(pluginContext); + const isCoreGrafanaPlugin = pluginContext?.meta.module.startsWith('core:') ?? false; const components: Array> = []; const extensionsByPlugin: Record = {}; const pluginId = pluginContext?.meta.id ?? ''; @@ -38,7 +39,10 @@ export function usePluginComponents({ }); // Don't show extensions if the extension-point id is invalid in DEV mode - if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { + if ( + isGrafanaDevMode() && + !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, isCoreGrafanaPlugin, log: pointLog }) + ) { return { isLoading: false, components: [], @@ -46,7 +50,12 @@ export function usePluginComponents({ } // Don't show extensions if the extension-point misses meta info (plugin.json) in DEV mode - if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if ( + isGrafanaDevMode() && + !isCoreGrafanaPlugin && + pluginContext && + isExtensionPointMetaInfoMissing(extensionPointId, pluginContext) + ) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/usePluginFunctions.tsx b/public/app/features/plugins/extensions/usePluginFunctions.tsx index 18a8abd7aef..8f8c989e316 100644 --- a/public/app/features/plugins/extensions/usePluginFunctions.tsx +++ b/public/app/features/plugins/extensions/usePluginFunctions.tsx @@ -24,6 +24,7 @@ export function usePluginFunctions({ return useMemo(() => { const isInsidePlugin = Boolean(pluginContext); + const isCoreGrafanaPlugin = pluginContext?.meta.module.startsWith('core:') ?? false; const results: Array> = []; const extensionsByPlugin: Record = {}; const pluginId = pluginContext?.meta.id ?? ''; @@ -32,14 +33,22 @@ export function usePluginFunctions({ extensionPointId, }); - if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { + if ( + isGrafanaDevMode() && + !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, isCoreGrafanaPlugin, log: pointLog }) + ) { return { isLoading: false, functions: [], }; } - if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if ( + isGrafanaDevMode() && + !isCoreGrafanaPlugin && + pluginContext && + isExtensionPointMetaInfoMissing(extensionPointId, pluginContext) + ) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/usePluginLinks.test.tsx b/public/app/features/plugins/extensions/usePluginLinks.test.tsx index b5d984b165c..697c9dfd91f 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.test.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.test.tsx @@ -1,6 +1,13 @@ import { act, renderHook } from '@testing-library/react'; -import { PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data'; +import { + PluginContextProvider, + PluginExtensionPoints, + PluginLoadingStrategy, + PluginMeta, + PluginType, +} from '@grafana/data'; +import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; @@ -98,6 +105,32 @@ describe('usePluginLinks()', () => { }, }; + config.apps[pluginId] = { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + extensions: { + addedLinks: [], + addedComponents: [], + addedFunctions: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -219,6 +252,49 @@ describe('usePluginLinks()', () => { expect(log.warning).not.toHaveBeenCalled(); }); + // It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points. + it('should not validate the extension point meta-info for core plugins', () => { + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + const linkConfig = { + targets: extensionPointId, + title: '1', + description: '1', + path: `/a/${pluginId}/2`, + }; + + // The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps). + config.apps[pluginId].extensions.addedLinks = [linkConfig]; + + wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + registries.addedLinksRegistry.register({ + pluginId, + configs: [linkConfig], + }); + + // Trying to render an extension point that is not defined in the plugin meta + // (No restrictions due to being a core plugin) + let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); + expect(result.current.links.length).toBe(1); + expect(log.warning).not.toHaveBeenCalled(); + }); + it('should not validate the extension point id in production mode', () => { // Empty list of extension points in the plugin meta (from plugin.json) wrapper = ({ children }: { children: React.ReactNode }) => ( diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index adec8114582..5d2b193a77c 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -34,19 +34,28 @@ export function usePluginLinks({ return useMemo(() => { const isInsidePlugin = Boolean(pluginContext); const pluginId = pluginContext?.meta.id ?? ''; + const isCoreGrafanaPlugin = pluginContext?.meta.module.startsWith('core:') ?? false; const pointLog = log.child({ pluginId, extensionPointId, }); - if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { + if ( + isGrafanaDevMode() && + !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, isCoreGrafanaPlugin, log: pointLog }) + ) { return { isLoading: false, links: [], }; } - if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if ( + isGrafanaDevMode() && + !isCoreGrafanaPlugin && + pluginContext && + isExtensionPointMetaInfoMissing(extensionPointId, pluginContext) + ) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index 1b7e71eba7b..2f1713d3f34 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -217,6 +217,7 @@ describe('Plugin Extension Validators', () => { extensionPointId, pluginId, isInsidePlugin: pluginId !== 'grafana' && pluginId !== '', + isCoreGrafanaPlugin: false, log: createLogMock(), }) ).toBe(true); @@ -244,10 +245,23 @@ describe('Plugin Extension Validators', () => { extensionPointId, pluginId, isInsidePlugin: pluginId !== 'grafana' && pluginId !== '', + isCoreGrafanaPlugin: false, log: createLogMock(), }) ).toBe(false); }); + + it('should return FALSE true if the extension point id is set by a core plugin', () => { + expect( + isExtensionPointIdValid({ + extensionPointId: 'traces', + pluginId: 'traces', + isInsidePlugin: true, + isCoreGrafanaPlugin: true, + log: createLogMock(), + }) + ).toBe(true); + }); }); describe('isAddedLinkMetaInfoMissing()', () => { diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index 081ac98fbab..df808a34576 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -69,17 +69,19 @@ export function isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, + isCoreGrafanaPlugin, log, }: { extensionPointId: string; pluginId: string; isInsidePlugin: boolean; + isCoreGrafanaPlugin: boolean; log: ExtensionsLog; }) { const startsWithPluginId = extensionPointId.startsWith(`${pluginId}/`) || extensionPointId.startsWith(`plugins/${pluginId}/`); - if (isInsidePlugin && !startsWithPluginId) { + if (isInsidePlugin && !isCoreGrafanaPlugin && !startsWithPluginId) { log.error(errors.INVALID_EXTENSION_POINT_ID_PLUGIN(pluginId, extensionPointId)); return false; }