From 4c0dde6f2fe6f075b89eaf559cd3c9c9455ab286 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 12 Nov 2024 08:55:04 +0100 Subject: [PATCH] Plugin Extensions: Streamline log messages (#95943) * streamline log messages * cleanup * fix tests * only log errors to the console * more cleanup again * cleanup * Update public/app/features/plugins/extensions/errors.ts Co-authored-by: Levente Balogh * pr feedback * remove white space * Update public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts Co-authored-by: Levente Balogh * revert touched file * revert touched file * fix tests --------- Co-authored-by: Levente Balogh --- .../apis/folders/legacy_storage_test.go | 8 +- .../app/features/plugins/extensions/errors.ts | 42 ++ .../features/plugins/extensions/logs/log.ts | 3 +- .../registry/AddedComponentsRegistry.test.ts | 31 +- .../registry/AddedComponentsRegistry.ts | 17 +- .../registry/AddedLinksRegistry.test.ts | 8 +- .../extensions/registry/AddedLinksRegistry.ts | 32 +- ...t.ts => ExposedComponentsRegistry.test.ts} | 41 +- .../registry/ExposedComponentsRegistry.ts | 25 +- .../extensions/usePluginComponent.test.tsx | 2 +- .../plugins/extensions/usePluginComponent.tsx | 10 +- .../extensions/usePluginComponents.test.tsx | 14 +- .../extensions/usePluginComponents.tsx | 19 +- .../extensions/usePluginExtensions.tsx | 15 +- .../extensions/usePluginLinks.test.tsx | 4 +- .../plugins/extensions/usePluginLinks.tsx | 28 +- .../plugins/extensions/utils.test.tsx | 438 +--------------- .../app/features/plugins/extensions/utils.tsx | 134 ----- .../plugins/extensions/validators.test.tsx | 484 +++++++++++++++++- .../features/plugins/extensions/validators.ts | 123 ++++- .../dataquery.cue | 2 +- 21 files changed, 737 insertions(+), 743 deletions(-) create mode 100644 public/app/features/plugins/extensions/errors.ts rename public/app/features/plugins/extensions/registry/{ExportedComponentsRegistry.test.ts => ExposedComponentsRegistry.test.ts} (89%) diff --git a/pkg/registry/apis/folders/legacy_storage_test.go b/pkg/registry/apis/folders/legacy_storage_test.go index 76489ca8781..eb3c494f49d 100644 --- a/pkg/registry/apis/folders/legacy_storage_test.go +++ b/pkg/registry/apis/folders/legacy_storage_test.go @@ -21,10 +21,10 @@ func TestLegacyStorageList(t *testing.T) { folderService := &foldertest.FakeService{} folderService.ExpectedFolders = []*folder.Folder{ - &folder.Folder{UID: "parent", Title: "Folder Parent", ParentUID: ""}, - &folder.Folder{UID: "child", Title: "Folder Child", ParentUID: "parent"}, - &folder.Folder{UID: "anotherparent1", Title: "Folder Another Parent 1", ParentUID: ""}, - &folder.Folder{UID: "anotherparent1", Title: "Folder Another Parent 2", ParentUID: ""}, + {UID: "parent", Title: "Folder Parent", ParentUID: ""}, + {UID: "child", Title: "Folder Child", ParentUID: "parent"}, + {UID: "anotherparent1", Title: "Folder Another Parent 1", ParentUID: ""}, + {UID: "anotherparent1", Title: "Folder Another Parent 2", ParentUID: ""}, } usr := &user.SignedInUser{UserID: 1} diff --git a/public/app/features/plugins/extensions/errors.ts b/public/app/features/plugins/extensions/errors.ts new file mode 100644 index 00000000000..39c65df0f8e --- /dev/null +++ b/public/app/features/plugins/extensions/errors.ts @@ -0,0 +1,42 @@ +export const INVALID_EXTENSION_POINT_ID = + 'Invalid usage of extension point. Reason: Extension point id should be prefixed with your plugin id, e.g "myorg-foo-app/toolbar/v1".'; + +export const EXTENSION_POINT_META_INFO_MISSING = + 'Invalid usage of extension point. Reason: The extension point is not recorded in the "plugin.json" file. Extension points must be listed in the section "extensions.extensionPoints[]". Returning an empty array of extensions.'; + +export const TITLE_MISSING = 'Title is missing.'; + +export const DESCRIPTION_MISSING = 'Description is missing.'; + +export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.'; + +export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.'; + +export const INVALID_PATH = 'The "path" is required and should start with "/a/".'; + +export const INVALID_EXPOSED_COMPONENT_ID = + "The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'."; + +export const EXPOSED_COMPONENT_ALREADY_EXISTS = 'An exposed component with the same id already exists.'; + +export const EXPOSED_COMPONENT_META_INFO_MISSING = + 'The exposed component was not recorded in the plugin.json. Exposed component extensions must be listed in the section "extensions.exposedComponents[]". Currently, this is only required in development but will be enforced also in production builds in the future.'; + +export const EXPOSED_COMPONENT_DEPENDENCY_MISSING = + 'Invalid usage of extension point. Reason: The exposed component is not recorded in the "plugin.json" file. Exposed components must be listed in the dependencies[] section.'; + +export const ADDED_COMPONENT_META_INFO_MISSING = + 'The extension was not recorded in the plugin.json. Added component extensions must be listed in the section "extensions.addedComponents[]". Currently, this is only required in development but will be enforced also in production builds in the future.'; + +export const TITLE_NOT_MATCHING_META_INFO = 'The "title" doesn\'t match the title recorded in plugin.json.'; + +export const ADDED_LINK_META_INFO_MISSING = + 'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.'; + +export const DESCRIPTION_NOT_MATCHING_META_INFO = + 'The "description" doesn\'t match the description recorded in plugin.json.'; + +export const TARGET_NOT_MATCHING_META_INFO = + 'The "targets" for the registered extension does not match the targets recorded in plugin.json. Currently, this is only required in development but will be enforced also in production builds in the future.'; + +export const APP_NOT_FOUND = (pluginId: string) => `The app plugin with plugin id "${pluginId}" was not found.`; diff --git a/public/app/features/plugins/extensions/logs/log.ts b/public/app/features/plugins/extensions/logs/log.ts index 6638fbcaaf0..e66b7be4d15 100644 --- a/public/app/features/plugins/extensions/logs/log.ts +++ b/public/app/features/plugins/extensions/logs/log.ts @@ -3,6 +3,7 @@ import { nanoid } from 'nanoid'; import { Observable, ReplaySubject } from 'rxjs'; import { Labels, LogLevel } from '@grafana/data'; +import { config } from '@grafana/runtime'; export type ExtensionsLogItem = { level: LogLevel; @@ -32,7 +33,7 @@ export class ExtensionsLog { } warning(message: string, labels?: Labels): void { - console.warn(message, { ...this.baseLabels, ...labels }); + config.buildInfo.env === 'development' && console.warn(message, { ...this.baseLabels, ...labels }); this.log(LogLevel.warning, message, labels); } diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index d04a137e52d..e82c48dcee0 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -357,29 +357,6 @@ describe('AddedComponentsRegistry', () => { }); }); - it('should log a warning when added component id is not suffixed with component version', async () => { - const registry = new AddedComponentsRegistry(); - const extensionPointId = 'grafana/test/home'; - - registry.register({ - pluginId, - configs: [ - { - title: 'Component 1 title', - description: 'Component 1 description', - targets: [extensionPointId], - component: () => React.createElement('div', null, 'Hello World1'), - }, - ], - }); - - expect(log.warning).toHaveBeenCalledWith( - `Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.` - ); - const currentState = await registry.getState(); - expect(Object.keys(currentState)).toHaveLength(1); - }); - it('should not register component when title is missing', async () => { const registry = new AddedComponentsRegistry(); const extensionPointId = 'grafana/alerting/home'; @@ -396,7 +373,7 @@ describe('AddedComponentsRegistry', () => { ], }); - expect(log.error).toHaveBeenCalledWith('Could not register added component. Reason: Title is missing.'); + expect(log.error).toHaveBeenCalledWith('Could not register component extension. Reason: Title is missing.'); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); @@ -482,7 +459,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should register a component added by a core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -505,7 +482,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register a component added by a plugin in production mode even if the meta-info is missing', async () => { @@ -531,7 +508,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register a component added by a plugin in dev-mode if the meta-info is present', async () => { diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts index f46d233f987..d4b8b0b04f5 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts @@ -2,11 +2,14 @@ import { ReplaySubject } from 'rxjs'; import { PluginExtensionAddedComponentConfig } from '@grafana/data'; -import { isAddedComponentMetaInfoMissing, isGrafanaDevMode, wrapWithPluginContext } from '../utils'; -import { extensionPointEndsWithVersion, isGrafanaCoreExtensionPoint } from '../validators'; +import * as errors from '../errors'; +import { isGrafanaDevMode, wrapWithPluginContext } from '../utils'; +import { isAddedComponentMetaInfoMissing } from '../validators'; import { PluginExtensionConfigs, Registry, RegistryType } from './Registry'; +const logPrefix = 'Could not register component extension. Reason:'; + export type AddedComponentRegistryItem = { pluginId: string; title: string; @@ -41,7 +44,7 @@ export class AddedComponentsRegistry extends Registry< }); if (!config.title) { - configLog.error(`Could not register added component. Reason: Title is missing.`); + configLog.error(`${logPrefix} ${errors.TITLE_MISSING}`); continue; } @@ -57,12 +60,6 @@ export class AddedComponentsRegistry extends Registry< for (const extensionPointId of extensionPointIds) { const pointIdLog = configLog.child({ extensionPointId }); - if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) { - pointIdLog.warning( - `Added component "${config.title}": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.` - ); - } - const result = { pluginId, component: wrapWithPluginContext(pluginId, config.component, pointIdLog), @@ -70,7 +67,7 @@ export class AddedComponentsRegistry extends Registry< title: config.title, }; - pointIdLog.debug(`Added component from '${pluginId}' to '${extensionPointId}'`); + pointIdLog.debug('Added component extension successfully registered'); if (!(extensionPointId in registry)) { registry[extensionPointId] = [result]; diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts index 3dd5b7c7f7d..4d5ab5c084f 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts @@ -661,7 +661,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should register a link added by core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -685,7 +685,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register a link added by a plugin in production mode even if the meta-info is missing', async () => { @@ -712,7 +712,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register a link added by a plugin in dev-mode if the meta-info is present', async () => { @@ -739,6 +739,6 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts index d7e66acf798..52a1ad9a96c 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts @@ -3,16 +3,14 @@ import { ReplaySubject } from 'rxjs'; import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data'; import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/src/types/pluginExtensions'; -import { isAddedLinkMetaInfoMissing, isGrafanaDevMode } from '../utils'; -import { - extensionPointEndsWithVersion, - isConfigureFnValid, - isGrafanaCoreExtensionPoint, - isLinkPathValid, -} from '../validators'; +import * as errors from '../errors'; +import { isGrafanaDevMode } from '../utils'; +import { isAddedLinkMetaInfoMissing, isConfigureFnValid, isLinkPathValid } from '../validators'; import { PluginExtensionConfigs, Registry, RegistryType } from './Registry'; +const logPrefix = 'Could not register link extension. Reason:'; + export type AddedLinkRegistryItem = { pluginId: string; extensionPointId: string; @@ -52,29 +50,26 @@ export class AddedLinksRegistry extends Registry { }); }); - it('should log a warning if another component with the same id already exists in the registry', async () => { + it('should log an error if another component with the same id already exists in the registry', async () => { const registry = new ExposedComponentsRegistry(); registry.register({ pluginId: 'grafana-basic-app1', @@ -304,13 +304,13 @@ describe('ExposedComponentsRegistry', () => { }); expect(log.error).toHaveBeenCalledWith( - "Could not register exposed component with 'grafana-basic-app1/hello-world/v1'. Reason: An exposed component with the same id already exists." + 'Could not register exposed component. Reason: An exposed component with the same id already exists.' ); const currentState2 = await registry.getState(); expect(Object.keys(currentState2)).toHaveLength(1); }); - it('should skip registering component and log a warning when id is not prefixed with plugin id', async () => { + it('should skip registering component and log an error when id is not prefixed with plugin id', async () => { const registry = new ExposedComponentsRegistry(); registry.register({ pluginId: 'grafana-basic-app1', @@ -325,33 +325,12 @@ describe('ExposedComponentsRegistry', () => { }); expect(log.error).toHaveBeenCalledWith( - "Could not register exposed component with 'hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." + "Could not register exposed component. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." ); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); }); - it('should log a error when exposed component id is not suffixed with component version', async () => { - const registry = new ExposedComponentsRegistry(); - registry.register({ - pluginId: 'grafana-basic-app1', - configs: [ - { - id: 'grafana-basic-app1/hello-world', - title: 'not important', - description: 'not important', - component: () => React.createElement('div', null, 'Hello World1'), - }, - ], - }); - - expect(log.error).toHaveBeenCalledWith( - "Exposed component does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'." - ); - const currentState = await registry.getState(); - expect(Object.keys(currentState)).toHaveLength(1); - }); - it('should not register component when title is missing', async () => { const registry = new ExposedComponentsRegistry(); @@ -367,9 +346,7 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(log.error).toHaveBeenCalledWith( - "Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Title is missing." - ); + expect(log.error).toHaveBeenCalledWith('Could not register exposed component. Reason: Title is missing.'); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); @@ -455,7 +432,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should register an exposed component added by a core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -478,7 +455,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register an exposed component added by a plugin in production mode even if the meta-info is missing', async () => { @@ -504,7 +481,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should register an exposed component added by a plugin in dev-mode if the meta-info is present', async () => { @@ -530,6 +507,6 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts index 6f4cee8e6c1..9c13260518a 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts @@ -2,11 +2,14 @@ import { ReplaySubject } from 'rxjs'; import { PluginExtensionExposedComponentConfig } from '@grafana/data'; -import { isExposedComponentMetaInfoMissing, isGrafanaDevMode } from '../utils'; -import { extensionPointEndsWithVersion } from '../validators'; +import * as errors from '../errors'; +import { isGrafanaDevMode } from '../utils'; +import { isExposedComponentMetaInfoMissing } from '../validators'; import { Registry, RegistryType, PluginExtensionConfigs } from './Registry'; +const logPrefix = 'Could not register exposed component. Reason:'; + export type ExposedComponentRegistryItem = { pluginId: string; title: string; @@ -45,27 +48,17 @@ export class ExposedComponentsRegistry extends Registry< }); if (!id.startsWith(pluginId)) { - pointIdLog.error( - `Could not register exposed component with '${id}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'.` - ); + pointIdLog.error(`${logPrefix} ${errors.INVALID_EXPOSED_COMPONENT_ID}`); continue; } - if (!extensionPointEndsWithVersion(id)) { - pointIdLog.error( - `Exposed component does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.` - ); - } - if (registry[id]) { - pointIdLog.error( - `Could not register exposed component with '${id}'. Reason: An exposed component with the same id already exists.` - ); + pointIdLog.error(`${logPrefix} ${errors.EXPOSED_COMPONENT_ALREADY_EXISTS}`); continue; } if (!title) { - pointIdLog.error(`Could not register exposed component with id '${id}'. Reason: Title is missing.`); + pointIdLog.error(`${logPrefix} ${errors.TITLE_MISSING}`); continue; } @@ -77,7 +70,7 @@ export class ExposedComponentsRegistry extends Registry< continue; } - pointIdLog.debug(`Exposed component from '${pluginId}' to '${id}'`); + pointIdLog.debug('Exposed component extension successfully registered'); registry[id] = { ...config, pluginId }; } diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 365dd520e91..007414ab4dc 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -288,7 +288,7 @@ describe('usePluginComponent()', () => { // Shouldn't return the component, as it's not present in the plugin.json dependencies let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); expect(result.current.component).toBe(null); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should return the exposed component if the meta-info is correct and in dev mode', () => { diff --git a/public/app/features/plugins/extensions/usePluginComponent.tsx b/public/app/features/plugins/extensions/usePluginComponent.tsx index c93ff04b10a..ba057721f26 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.tsx @@ -5,8 +5,10 @@ import { usePluginContext } from '@grafana/data'; import { UsePluginComponentResult } from '@grafana/runtime'; import { useExposedComponentsRegistry } from './ExtensionRegistriesContext'; +import * as errors from './errors'; import { log } from './logs/log'; -import { isExposedComponentDependencyMissing, isGrafanaDevMode, wrapWithPluginContext } from './utils'; +import { isGrafanaDevMode, wrapWithPluginContext } from './utils'; +import { isExposedComponentDependencyMissing } from './validators'; // Returns a component exposed by a plugin. // (Exposed components can be defined in plugins by calling .exposeComponent() on the AppPlugin instance.) @@ -33,10 +35,8 @@ export function usePluginComponent(id: string): UsePl pluginId: registryItem.pluginId, }); - if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext, componentLog)) { - componentLog.warning( - `usePluginComponent("${id}") - The exposed component ("${id}") is missing from the dependencies[] in the "plugin.json" file.` - ); + if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext)) { + componentLog.error(errors.EXPOSED_COMPONENT_DEPENDENCY_MISSING); return { isLoading: false, component: null, diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index d174a871cc7..e873e6e81a7 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -262,7 +262,7 @@ describe('usePluginComponents()', () => { // (No restrictions due to isGrafanaDevMode() = false) let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should not validate the extension point id in production mode', () => { @@ -287,7 +287,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(0); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should not validate the extension point meta-info if used in Grafana core (no plugin context)', () => { @@ -316,7 +316,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(1); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should not validate the extension point id if used in Grafana core (no plugin context)', () => { @@ -332,7 +332,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(0); - expect(log.warning).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); }); it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -370,10 +370,10 @@ describe('usePluginComponents()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); - it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { + it('should not log an error if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { // Imitate running in dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); @@ -414,6 +414,6 @@ describe('usePluginComponents()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index 45a6c6d9487..3147a8441d9 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -8,9 +8,10 @@ import { } from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; import { useAddedComponentsRegistry } from './ExtensionRegistriesContext'; +import * as errors from './errors'; import { log } from './logs/log'; -import { isExtensionPointMetaInfoMissing, isGrafanaDevMode } from './utils'; -import { isExtensionPointIdValid } from './validators'; +import { isGrafanaDevMode } from './utils'; +import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators'; // Returns an array of component extensions for the given extension point export function usePluginComponents({ @@ -33,19 +34,11 @@ export function usePluginComponents({ }); if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.warning( - `Extension point usePluginComponents("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` - ); - return { - isLoading: false, - components: [], - }; + pointLog.error(errors.INVALID_EXTENSION_POINT_ID); } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { - pointLog.warning( - `usePluginComponents("${extensionPointId}") - The extension point is missing from the "plugin.json" file.` - ); + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, components: [], diff --git a/public/app/features/plugins/extensions/usePluginExtensions.tsx b/public/app/features/plugins/extensions/usePluginExtensions.tsx index bb041790b3e..5a7050062e2 100644 --- a/public/app/features/plugins/extensions/usePluginExtensions.tsx +++ b/public/app/features/plugins/extensions/usePluginExtensions.tsx @@ -4,11 +4,12 @@ import { useObservable } from 'react-use'; import { PluginExtension, usePluginContext } from '@grafana/data'; import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/runtime'; +import * as errors from './errors'; import { getPluginExtensions } from './getPluginExtensions'; import { log } from './logs/log'; import { PluginExtensionRegistries } from './registry/types'; -import { isExtensionPointMetaInfoMissing, isGrafanaDevMode } from './utils'; -import { isExtensionPointIdValid } from './validators'; +import { isGrafanaDevMode } from './utils'; +import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators'; export function createUsePluginExtensions(registries: PluginExtensionRegistries) { const observableAddedComponentsRegistry = registries.addedComponentsRegistry.asObservable(); @@ -34,19 +35,15 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries) } if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.warning( - `Extension point usePluginExtensions("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` - ); + pointLog.error(errors.INVALID_EXTENSION_POINT_ID); return { isLoading: false, extensions: [], }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { - pointLog.warning( - `Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"` - ); + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, extensions: [], diff --git a/public/app/features/plugins/extensions/usePluginLinks.test.tsx b/public/app/features/plugins/extensions/usePluginLinks.test.tsx index 8fd92bd944a..c9d6ca2af39 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.test.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.test.tsx @@ -307,7 +307,7 @@ describe('usePluginLinks()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -345,6 +345,6 @@ describe('usePluginLinks()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(log.warning).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 8a50b558e0e..1a7205e347a 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -9,6 +9,7 @@ import { } from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; import { useAddedLinksRegistry } from './ExtensionRegistriesContext'; +import * as errors from './errors'; import { log } from './logs/log'; import { generateExtensionId, @@ -16,10 +17,9 @@ import { getLinkExtensionOverrides, getLinkExtensionPathWithTracking, getReadOnlyProxy, - isExtensionPointMetaInfoMissing, isGrafanaDevMode, } from './utils'; -import { isExtensionPointIdValid } from './validators'; +import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators'; // Returns an array of component extensions for the given extension point export function usePluginLinks({ @@ -41,19 +41,15 @@ export function usePluginLinks({ }); if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.warning( - `Extension point usePluginLinks("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` - ); + pointLog.error(errors.INVALID_EXTENSION_POINT_ID); return { isLoading: false, links: [], }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { - pointLog.warning( - `Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"` - ); + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, links: [], @@ -73,8 +69,16 @@ export function usePluginLinks({ for (const addedLink of registryState[extensionPointId] ?? []) { const { pluginId } = addedLink; + const linkLog = pointLog.child({ + path: addedLink.path ?? '', + title: addedLink.title, + description: addedLink.description ?? '', + onClick: typeof addedLink.onClick, + }); + // Only limit if the `limitPerPlugin` is set if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) { + linkLog.debug(`Skipping link extension from plugin "${pluginId}". Reason: Limit reached.`); continue; } @@ -82,12 +86,6 @@ export function usePluginLinks({ extensionsByPlugin[pluginId] = 0; } - const linkLog = pointLog.child({ - path: addedLink.path ?? '', - title: addedLink.title, - description: addedLink.description ?? '', - onClick: typeof addedLink.onClick, - }); // Run the configure() function with the current context, and apply the ovverides const overrides = getLinkExtensionOverrides(pluginId, addedLink, linkLog, frozenContext); diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 6da25110752..b78afe339ba 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -1,31 +1,17 @@ import { render, screen } from '@testing-library/react'; import { type Unsubscribable } from 'rxjs'; -import { - dateTime, - PluginContextType, - PluginExtensionPoints, - PluginLoadingStrategy, - PluginType, - usePluginContext, -} from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { dateTime, usePluginContext } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { log } from './logs/log'; -import { createLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, getReadOnlyProxy, createOpenModalFunction, wrapWithPluginContext, - isAddedLinkMetaInfoMissing, - isAddedComponentMetaInfoMissing, - isExposedComponentMetaInfoMissing, - isExposedComponentDependencyMissing, - isExtensionPointMetaInfoMissing, } from './utils'; jest.mock('app/features/plugins/pluginSettings', () => ({ @@ -461,426 +447,4 @@ describe('Plugin Extensions / Utils', () => { expect(screen.getByText('Version: 1.0.0')).toBeVisible(); }); }); - - describe('isAddedLinkMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - 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: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - const extensionConfig = { - targets: [PluginExtensionPoints.DashboardPanelMenu], - title: 'Link title', - description: 'Link description', - }; - - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); - - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(false); - expect(log.warning).toHaveBeenCalledTimes(0); - }); - - it('should return TRUE and log a warning if the app config is not found', () => { - const log = createLogMock(); - delete config.apps[pluginId]; - - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); - }); - - it('should return TRUE and log a warning if the link has no meta-info in the plugin.json', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks = []; - - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); - }); - - it('should return TRUE and log a warning if the "targets" do not match', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); - - const returnValue = isAddedLinkMetaInfoMissing( - pluginId, - { - ...extensionConfig, - targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction], - }, - log - ); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"targets" don\'t match'); - }); - }); - - describe('isAddedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - 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: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - const extensionConfig = { - targets: [PluginExtensionPoints.DashboardPanelMenu], - title: 'Component title', - description: 'Component description', - component: () =>
Component content
, - }; - - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); - - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(false); - expect(log.warning).toHaveBeenCalledTimes(0); - }); - - it('should return TRUE and log a warning if the app config is not found', () => { - const log = createLogMock(); - delete config.apps[pluginId]; - - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); - }); - - it('should return TRUE and log a warning if the Component has no meta-info in the plugin.json', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents = []; - - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); - }); - - it('should return TRUE and log a warning if the "targets" do not match', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); - - const returnValue = isAddedComponentMetaInfoMissing( - pluginId, - { - ...extensionConfig, - targets: [PluginExtensionPoints.ExploreToolbarAction], - }, - log - ); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"targets" don\'t match'); - }); - }); - - describe('isExposedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; - const pluginId = 'myorg-extensions-app'; - const appPluginConfig = { - 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: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - const exposedComponentConfig = { - id: `${pluginId}/component/v1`, - title: 'Exposed component', - description: 'Exposed component description', - component: () =>
Component content
, - }; - - beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; - }); - - afterEach(() => { - config.apps = originalApps; - }); - - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); - - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); - - expect(returnValue).toBe(false); - expect(log.warning).toHaveBeenCalledTimes(0); - }); - - it('should return TRUE and log a warning if the app config is not found', () => { - const log = createLogMock(); - delete config.apps[pluginId]; - - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); - }); - - it('should return TRUE and log a warning if the exposed component has no meta-info in the plugin.json', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents = []; - - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); - }); - - it('should return TRUE and log a warning if the title does not match', () => { - const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); - - const returnValue = isExposedComponentMetaInfoMissing( - pluginId, - { - ...exposedComponentConfig, - title: 'UPDATED', - }, - log - ); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"title" doesn\'t match'); - }); - }); - - describe('isExposedComponentDependencyMissing()', () => { - let pluginContext: PluginContextType; - const pluginId = 'myorg-extensions-app'; - const exposedComponentId = `${pluginId}/component/v1`; - - beforeEach(() => { - pluginContext = { - meta: { - id: pluginId, - name: 'Extensions App', - type: PluginType.app, - module: '', - baseUrl: '', - info: { - author: { - name: 'MyOrg', - }, - description: 'App for testing extensions', - links: [], - logos: { - large: '', - small: '', - }, - screenshots: [], - updated: '2023-10-26T18:25:01Z', - version: '1.0.0', - }, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - }, - }; - }); - - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - const log = createLogMock(); - pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId); - - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); - - expect(returnValue).toBe(false); - expect(log.warning).toHaveBeenCalledTimes(0); - }); - - it('should return TRUE and log a warning if the dependencies are missing', () => { - const log = createLogMock(); - delete pluginContext.meta.dependencies; - - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); - }); - - it('should return TRUE and log a warning if the exposed component id is not specified in the list of dependencies', () => { - const log = createLogMock(); - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); - }); - }); - - describe('isExtensionPointMetaInfoMissing()', () => { - let pluginContext: PluginContextType; - const pluginId = 'myorg-extensions-app'; - const extensionPointId = `${pluginId}/extension-point/v1`; - const extensionPointConfig = { - id: extensionPointId, - title: 'Extension point title', - description: 'Extension point description', - }; - - beforeEach(() => { - pluginContext = { - meta: { - id: pluginId, - name: 'Extensions App', - type: PluginType.app, - module: '', - baseUrl: '', - info: { - author: { - name: 'MyOrg', - }, - description: 'App for testing extensions', - links: [], - logos: { - large: '', - small: '', - }, - screenshots: [], - updated: '2023-10-26T18:25:01Z', - version: '1.0.0', - }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - }, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - }, - }; - }); - - it('should return FALSE if the meta-info in the plugin.json is correct', () => { - const log = createLogMock(); - pluginContext.meta.extensions?.extensionPoints.push(extensionPointConfig); - - const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, log); - - expect(returnValue).toBe(false); - expect(log.warning).toHaveBeenCalledTimes(0); - }); - - it('should return TRUE and log a warning if the extension point id is not recorded in the plugin.json', () => { - const log = createLogMock(); - const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, log); - - expect(returnValue).toBe(true); - expect(log.warning).toHaveBeenCalledTimes(1); - expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Extension point "${extensionPointId}"`); - }); - }); }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 6ce6b18bbfe..b4819a3f1b8 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -16,9 +16,6 @@ import { PanelMenuItem, PluginExtensionAddedLinkConfig, urlUtil, - PluginContextType, - PluginExtensionExposedComponentConfig, - PluginExtensionAddedComponentConfig, } from '@grafana/data'; import { reportInteraction, config } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; @@ -424,134 +421,3 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string, // Comes from the `app_mode` setting in the Grafana config (defaults to "development") // Can be set with the `GF_DEFAULT_APP_MODE` environment variable export const isGrafanaDevMode = () => config.buildInfo.env === 'development'; - -// Checks if the meta information is missing from the plugin's plugin.json file -export const isExtensionPointMetaInfoMissing = ( - extensionPointId: string, - pluginContext: PluginContextType, - log: ExtensionsLog -) => { - const pluginId = pluginContext.meta?.id; - const extensionPoints = pluginContext.meta?.extensions?.extensionPoints; - - if (!extensionPoints || !extensionPoints.some((ep) => ep.id === extensionPointId)) { - log.warning( - `Extension point "${extensionPointId}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "extensions.extensionPoints[]".` - ); - return true; - } - - return false; -}; - -// Checks if an exposed component that the plugin is depending on is missing from the `dependencies` in the plugin.json file -export const isExposedComponentDependencyMissing = ( - id: string, - pluginContext: PluginContextType, - log: ExtensionsLog -) => { - const pluginId = pluginContext.meta?.id; - const exposedComponentsDependencies = pluginContext.meta?.dependencies?.extensions?.exposedComponents; - - if (!exposedComponentsDependencies || !exposedComponentsDependencies.includes(id)) { - log.warning( - `Using exposed component "${id}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "dependencies.extensions.exposedComponents[]".` - ); - return true; - } - - return false; -}; - -export const isAddedLinkMetaInfoMissing = ( - pluginId: string, - metaInfo: PluginExtensionAddedLinkConfig, - log: ExtensionsLog -) => { - const app = config.apps[pluginId]; - const logPrefix = `Added-link "${metaInfo.title}" from "${pluginId}" -`; - const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null; - - if (!app) { - log.warning(`${logPrefix} couldn't find app plugin "${pluginId}"`); - return true; - } - - if (!pluginJsonMetaInfo) { - log.warning(`${logPrefix} not registered in the plugin.json under "extensions.addedLinks[]".`); - - return true; - } - - const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; - if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { - log.warning(`${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedLinks[]".`); - - return true; - } - - return false; -}; - -export const isAddedComponentMetaInfoMissing = ( - pluginId: string, - metaInfo: PluginExtensionAddedComponentConfig, - log: ExtensionsLog -) => { - const app = config.apps[pluginId]; - const logPrefix = `Added component "${metaInfo.title}" -`; - const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null; - - if (!app) { - log.warning(`${logPrefix} couldn't find app plugin "${pluginId}"`); - return true; - } - - if (!pluginJsonMetaInfo) { - log.warning(`${logPrefix} not registered in the plugin.json under "extensions.addedComponents[]".`); - - return true; - } - - const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; - if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { - log.warning( - `${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedComponents[]".` - ); - - return true; - } - - return false; -}; - -export const isExposedComponentMetaInfoMissing = ( - pluginId: string, - metaInfo: PluginExtensionExposedComponentConfig, - log: ExtensionsLog -) => { - const app = config.apps[pluginId]; - const logPrefix = `Exposed component "${metaInfo.id}" -`; - const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null; - - if (!app) { - log.warning(`${logPrefix} couldn't find app plugin: "${pluginId}"`); - return true; - } - - if (!pluginJsonMetaInfo) { - log.warning(`${logPrefix} not registered in the plugin.json under "extensions.exposedComponents[]".`); - - return true; - } - - if (pluginJsonMetaInfo.title !== metaInfo.title) { - log.warning( - `${logPrefix} the "title" doesn't match with one in the plugin.json under "extensions.exposedComponents[]".` - ); - - return true; - } - - return false; -}; diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index 3e968018e45..12b146bbf5c 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -1,12 +1,26 @@ import { memo } from 'react'; -import { PluginExtensionAddedLinkConfig, PluginExtensionLinkConfig, PluginExtensionPoints } from '@grafana/data'; +import { + PluginContextType, + PluginExtensionAddedLinkConfig, + PluginExtensionLinkConfig, + PluginExtensionPoints, + PluginLoadingStrategy, + PluginType, +} from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { createLogMock } from './logs/testUtils'; import { assertConfigureIsValid, assertLinkPathIsValid, assertStringProps, + isAddedComponentMetaInfoMissing, + isAddedLinkMetaInfoMissing, + isExposedComponentDependencyMissing, + isExposedComponentMetaInfoMissing, isExtensionPointIdValid, + isExtensionPointMetaInfoMissing, isGrafanaCoreExtensionPoint, isReactComponent, } from './validators'; @@ -231,4 +245,472 @@ describe('Plugin Extension Validators', () => { ).toBe(false); }); }); + + describe('isAddedLinkMetaInfoMissing()', () => { + const originalApps = config.apps; + const pluginId = 'myorg-extensions-app'; + const appPluginConfig = { + 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: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + const extensionConfig = { + targets: [PluginExtensionPoints.DashboardPanelMenu], + title: 'Link title', + description: 'Link description', + }; + + beforeEach(() => { + config.apps = { + [pluginId]: appPluginConfig, + }; + }); + + afterEach(() => { + config.apps = originalApps; + }); + + it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(false); + expect(log.error).toHaveBeenCalledTimes(0); + }); + + it('should return TRUE and log an error if the app config is not found', () => { + const log = createLogMock(); + delete config.apps[pluginId]; + + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch('The app plugin with plugin id'); + }); + + it('should return TRUE and log an error if the link has no meta-info in the plugin.json', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedLinks = []; + + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]"' + ); + }); + + it('should return TRUE and log an error if the "targets" do not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + + const returnValue = isAddedLinkMetaInfoMissing( + pluginId, + { + ...extensionConfig, + targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction], + }, + log + ); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The "targets" for the registered extension does not match' + ); + }); + + it('should return FALSE and log a warning if the "description" does not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + + const returnValue = isAddedLinkMetaInfoMissing( + pluginId, + { + ...extensionConfig, + description: 'Link description UPDATED', + }, + log + ); + + expect(returnValue).toBe(false); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); + }); + }); + + describe('isAddedComponentMetaInfoMissing()', () => { + const originalApps = config.apps; + const pluginId = 'myorg-extensions-app'; + const appPluginConfig = { + 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: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + const extensionConfig = { + targets: [PluginExtensionPoints.DashboardPanelMenu], + title: 'Component title', + description: 'Component description', + component: () =>
Component content
, + }; + + beforeEach(() => { + config.apps = { + [pluginId]: appPluginConfig, + }; + }); + + afterEach(() => { + config.apps = originalApps; + }); + + it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(false); + expect(log.error).toHaveBeenCalledTimes(0); + }); + + it('should return TRUE and log an error if the app config is not found', () => { + const log = createLogMock(); + delete config.apps[pluginId]; + + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch('The app plugin with plugin id'); + }); + + it('should return TRUE and log an error if the Component has no meta-info in the plugin.json', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedComponents = []; + + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The extension was not recorded in the plugin.json. Added component extensions must be listed in the section "extensions.addedComponents[]"' + ); + }); + + it('should return TRUE and log an error if the "targets" do not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + + const returnValue = isAddedComponentMetaInfoMissing( + pluginId, + { + ...extensionConfig, + targets: [PluginExtensionPoints.ExploreToolbarAction], + }, + log + ); + + expect(returnValue).toBe(true); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The "targets" for the registered extension does not match' + ); + }); + + it('should return FALSE and log a warning if the "description" does not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + + const returnValue = isAddedComponentMetaInfoMissing( + pluginId, + { + ...extensionConfig, + description: 'UPDATED', + }, + log + ); + + expect(returnValue).toBe(false); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); + }); + }); + + describe('isExposedComponentMetaInfoMissing()', () => { + const originalApps = config.apps; + const pluginId = 'myorg-extensions-app'; + const appPluginConfig = { + 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: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + const exposedComponentConfig = { + id: `${pluginId}/component/v1`, + title: 'Exposed component', + description: 'Exposed component description', + component: () =>
Component content
, + }; + + beforeEach(() => { + config.apps = { + [pluginId]: appPluginConfig, + }; + }); + + afterEach(() => { + config.apps = originalApps; + }); + + it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + + expect(returnValue).toBe(false); + expect(log.warning).toHaveBeenCalledTimes(0); + }); + + it('should return TRUE and log an error if the app config is not found', () => { + const log = createLogMock(); + delete config.apps[pluginId]; + + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch('The app plugin with plugin id'); + }); + + it('should return TRUE and log an error if the exposed component has no meta-info in the plugin.json', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.exposedComponents = []; + + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The exposed component was not recorded in the plugin.json. Exposed component extensions must be listed in the section "extensions.exposedComponents[]"' + ); + }); + + it('should return TRUE and log an error if the title does not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + + const returnValue = isExposedComponentMetaInfoMissing( + pluginId, + { + ...exposedComponentConfig, + title: 'UPDATED', + }, + log + ); + + expect(returnValue).toBe(true); + expect(log.error).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.error).mock.calls[0][0]).toMatch( + 'The "title" doesn\'t match the title recorded in plugin.json.' + ); + }); + + it('should return FALSE and log a warning if the "description" does not match', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + + const returnValue = isExposedComponentMetaInfoMissing( + pluginId, + { + ...exposedComponentConfig, + description: 'UPDATED', + }, + log + ); + + expect(returnValue).toBe(false); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); + }); + }); + + describe('isExposedComponentDependencyMissing()', () => { + let pluginContext: PluginContextType; + const pluginId = 'myorg-extensions-app'; + const exposedComponentId = `${pluginId}/component/v1`; + + beforeEach(() => { + pluginContext = { + meta: { + id: pluginId, + name: 'Extensions App', + type: PluginType.app, + module: '', + baseUrl: '', + info: { + author: { + name: 'MyOrg', + }, + description: 'App for testing extensions', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '2023-10-26T18:25:01Z', + version: '1.0.0', + }, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + }, + }; + }); + + it('should return FALSE if the meta-info in the plugin.json is correct', () => { + pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(false); + }); + + it('should return TRUE if the dependencies are missing', () => { + delete pluginContext.meta.dependencies; + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(true); + }); + + it('should return TRUE if the exposed component id is not specified in the list of dependencies', () => { + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + expect(returnValue).toBe(true); + }); + }); + + describe('isExtensionPointMetaInfoMissing()', () => { + let pluginContext: PluginContextType; + const pluginId = 'myorg-extensions-app'; + const extensionPointId = `${pluginId}/extension-point/v1`; + const extensionPointConfig = { + id: extensionPointId, + title: 'Extension point title', + description: 'Extension point description', + }; + + beforeEach(() => { + pluginContext = { + meta: { + id: pluginId, + name: 'Extensions App', + type: PluginType.app, + module: '', + baseUrl: '', + info: { + author: { + name: 'MyOrg', + }, + description: 'App for testing extensions', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '2023-10-26T18:25:01Z', + version: '1.0.0', + }, + extensions: { + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + }, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + }, + }; + }); + + it('should return FALSE if the meta-info in the plugin.json is correct', () => { + pluginContext.meta.extensions?.extensionPoints.push(extensionPointConfig); + + const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); + + expect(returnValue).toBe(false); + }); + + it('should return TRUE if the extension point id is not recorded in the plugin.json', () => { + const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); + expect(returnValue).toBe(true); + }); + }); }); diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index b03df733d83..b1dbd5e9af8 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -1,6 +1,16 @@ -import type { PluginExtensionAddedLinkConfig, PluginExtension, PluginExtensionLink } from '@grafana/data'; +import type { + PluginExtensionAddedLinkConfig, + PluginExtension, + PluginExtensionLink, + PluginContextType, + PluginExtensionAddedComponentConfig, + PluginExtensionExposedComponentConfig, +} from '@grafana/data'; import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions'; -import { isPluginExtensionLink } from '@grafana/runtime'; +import { config, isPluginExtensionLink } from '@grafana/runtime'; + +import * as errors from './errors'; +import { ExtensionsLog } from './logs/log'; export function assertPluginExtensionLink( extension: PluginExtension | undefined, @@ -103,3 +113,112 @@ export function isReactComponent(component: unknown): component is React.Compone // (The main reason is that we don't want to start depending on React implementation details.) return typeof component === 'function' || isReactMemoObject(component); } + +// Checks if the meta information is missing from the plugin's plugin.json file +export const isExtensionPointMetaInfoMissing = (extensionPointId: string, pluginContext: PluginContextType) => { + const extensionPoints = pluginContext.meta?.extensions?.extensionPoints; + + return !extensionPoints || !extensionPoints.some((ep) => ep.id === extensionPointId); +}; + +// Checks if an exposed component that the plugin is depending on is missing from the `dependencies` in the plugin.json file +export const isExposedComponentDependencyMissing = (id: string, pluginContext: PluginContextType) => { + const exposedComponentsDependencies = pluginContext.meta?.dependencies?.extensions?.exposedComponents; + + return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id); +}; + +export const isAddedLinkMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionAddedLinkConfig, + log: ExtensionsLog +) => { + const logPrefix = 'Could not register link extension. Reason:'; + const app = config.apps[pluginId]; + const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null; + + if (!app) { + log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); + return true; + } + + if (!pluginJsonMetaInfo) { + log.error(`${logPrefix} ${errors.ADDED_LINK_META_INFO_MISSING}`); + return true; + } + + const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; + if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); + return true; + } + + if (pluginJsonMetaInfo.description !== metaInfo.description) { + log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); + } + + return false; +}; + +export const isAddedComponentMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionAddedComponentConfig, + log: ExtensionsLog +) => { + const logPrefix = 'Could not register component extension. Reason:'; + const app = config.apps[pluginId]; + const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null; + + if (!app) { + log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); + return true; + } + + if (!pluginJsonMetaInfo) { + log.error(`${logPrefix} ${errors.ADDED_COMPONENT_META_INFO_MISSING}`); + return true; + } + + const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; + if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); + return true; + } + + if (pluginJsonMetaInfo.description !== metaInfo.description) { + log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); + } + + return false; +}; + +export const isExposedComponentMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionExposedComponentConfig, + log: ExtensionsLog +) => { + const logPrefix = 'Could not register exposed component extension. Reason:'; + const app = config.apps[pluginId]; + const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null; + + if (!app) { + log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); + return true; + } + + if (!pluginJsonMetaInfo) { + log.error(`${logPrefix} ${errors.EXPOSED_COMPONENT_META_INFO_MISSING}`); + return true; + } + + if (pluginJsonMetaInfo.title !== metaInfo.title) { + log.error(`${logPrefix} ${errors.TITLE_NOT_MATCHING_META_INFO}`); + return true; + } + + if (pluginJsonMetaInfo.description !== metaInfo.description) { + log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); + } + + return false; +}; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue index b5e6fb76790..67a62cc71e4 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue @@ -38,7 +38,7 @@ composableKinds: DataQuery: { // Allows to group the results. groupBy: [...string] // Sets the maximum number of time series. - limit?: int64 + limit?: int64 // Sets the maximum number of nodes in the flamegraph. maxNodes?: int64 #PyroscopeQueryType: "metrics" | "profile" | *"both" @cuetsy(kind="type")