Plugin Extensions: Support core plugins (#108685)
* feat(extensions): allow core plugins to use core grafana extension points * fix: don't validate the plugin.json for core plugins
This commit is contained in:
@@ -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 }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
@@ -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: () => <div>Component</div>,
|
||||
};
|
||||
|
||||
// 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 }) => (
|
||||
<PluginContextProvider
|
||||
meta={{
|
||||
...pluginMeta,
|
||||
// The module tells if it is a core plugin
|
||||
module: 'core:plugin/traces',
|
||||
extensions: {
|
||||
...pluginMeta.extensions!,
|
||||
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||
extensionPoints: [],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
</PluginContextProvider>
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
|
||||
@@ -29,6 +29,7 @@ export function usePluginComponents<Props extends object = {}>({
|
||||
|
||||
return useMemo(() => {
|
||||
const isInsidePlugin = Boolean(pluginContext);
|
||||
const isCoreGrafanaPlugin = pluginContext?.meta.module.startsWith('core:') ?? false;
|
||||
const components: Array<ComponentTypeWithExtensionMeta<Props>> = [];
|
||||
const extensionsByPlugin: Record<string, number> = {};
|
||||
const pluginId = pluginContext?.meta.id ?? '';
|
||||
@@ -38,7 +39,10 @@ export function usePluginComponents<Props extends object = {}>({
|
||||
});
|
||||
|
||||
// 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<Props extends object = {}>({
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -24,6 +24,7 @@ export function usePluginFunctions<Signature>({
|
||||
|
||||
return useMemo(() => {
|
||||
const isInsidePlugin = Boolean(pluginContext);
|
||||
const isCoreGrafanaPlugin = pluginContext?.meta.module.startsWith('core:') ?? false;
|
||||
const results: Array<PluginExtensionFunction<Signature>> = [];
|
||||
const extensionsByPlugin: Record<string, number> = {};
|
||||
const pluginId = pluginContext?.meta.id ?? '';
|
||||
@@ -32,14 +33,22 @@ export function usePluginFunctions<Signature>({
|
||||
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,
|
||||
|
||||
@@ -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 }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
@@ -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 }) => (
|
||||
<PluginContextProvider
|
||||
meta={{
|
||||
...pluginMeta,
|
||||
// The module tells if it is a core plugin
|
||||
module: 'core:plugin/traces',
|
||||
extensions: {
|
||||
...pluginMeta.extensions!,
|
||||
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||
extensionPoints: [],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
</PluginContextProvider>
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user