UI Extensions: Rename placement to extensionPointId (#65841)

* UI Extensions: Rename `placement` to `extensionPointId`

* Fix tests.

* Fix conflicts and review notes

* Remove `placement` from some other places
This commit is contained in:
Levente Balogh 2023-04-03 17:59:54 +02:00 committed by GitHub
parent 44ccd73d46
commit 1380fa54d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 126 additions and 124 deletions

View File

@ -54,7 +54,7 @@ export * from './accesscontrol';
export * from './icon'; export * from './icon';
export { export {
PluginExtensionTypes, PluginExtensionTypes,
PluginExtensionPlacements, PluginExtensionPoints,
type PluginExtension, type PluginExtension,
type PluginExtensionLink, type PluginExtensionLink,
type PluginExtensionConfig, type PluginExtensionConfig,

View File

@ -29,11 +29,11 @@ export type PluginExtensionConfig<Context extends object = object, ExtraProps ex
'title' | 'description' 'title' | 'description'
> & > &
ExtraProps & { ExtraProps & {
// The unique name of the placement // The unique identifier of the Extension Point
// Core Grafana placements are available in the `PluginExtensionPlacements` enum // (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
placement: string; extensionPointId: string;
// (Optional) A function that can be used to configure the extension dynamically based on the placement's context // (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
configure?: ( configure?: (
context?: Readonly<Context> context?: Readonly<Context>
) => Partial<{ title: string; description: string } & ExtraProps> | undefined; ) => Partial<{ title: string; description: string } & ExtraProps> | undefined;
@ -58,11 +58,11 @@ export type PluginExtensionEventHelpers<Context extends object = object> = {
}) => void; }) => void;
}; };
// Placements & Contexts // Extension Points & Contexts
// -------------------------------------------------------- // --------------------------------------------------------
// Placements available in core Grafana // Extension Points available in core Grafana
export enum PluginExtensionPlacements { export enum PluginExtensionPoints {
DashboardPanelMenu = 'grafana/dashboard/panel/menu', DashboardPanelMenu = 'grafana/dashboard/panel/menu',
} }

View File

@ -9,10 +9,10 @@ describe('Plugin Extensions / Get Plugin Extensions', () => {
const getter: GetPluginExtensions = jest.fn().mockReturnValue({ extensions: [] }); const getter: GetPluginExtensions = jest.fn().mockReturnValue({ extensions: [] });
setPluginExtensionGetter(getter); setPluginExtensionGetter(getter);
getPluginExtensions({ placement: 'panel-menu' }); getPluginExtensions({ extensionPointId: 'panel-menu' });
expect(getter).toHaveBeenCalledTimes(1); expect(getter).toHaveBeenCalledTimes(1);
expect(getter).toHaveBeenCalledWith({ placement: 'panel-menu' }); expect(getter).toHaveBeenCalledWith({ extensionPointId: 'panel-menu' });
}); });
test('should throw an error when trying to redefine the app-wide extension-getter function', () => { test('should throw an error when trying to redefine the app-wide extension-getter function', () => {
@ -33,7 +33,7 @@ describe('Plugin Extensions / Get Plugin Extensions', () => {
setPluginExtensionGetter(undefined); setPluginExtensionGetter(undefined);
expect(() => { expect(() => {
getPluginExtensions({ placement: 'panel-menu' }); getPluginExtensions({ extensionPointId: 'panel-menu' });
}).toThrowError(); }).toThrowError();
}); });
}); });

View File

@ -1,10 +1,10 @@
import { PluginExtension } from '@grafana/data'; import { PluginExtension } from '@grafana/data';
export type GetPluginExtensions = ({ export type GetPluginExtensions = ({
placement, extensionPointId,
context, context,
}: { }: {
placement: string; extensionPointId: string;
context?: object | Record<string | symbol, unknown>; context?: object | Record<string | symbol, unknown>;
}) => { }) => {
extensions: PluginExtension[]; extensions: PluginExtension[];

View File

@ -1,4 +1,4 @@
import { PanelMenuItem, PluginExtensionPlacements, type PluginExtensionPanelContext } from '@grafana/data'; import { PanelMenuItem, PluginExtensionPoints, type PluginExtensionPanelContext } from '@grafana/data';
import { import {
isPluginExtensionLink, isPluginExtensionLink,
AngularComponent, AngularComponent,
@ -279,7 +279,7 @@ export function getPanelMenu(
} }
const { extensions } = getPluginExtensions({ const { extensions } = getPluginExtensions({
placement: PluginExtensionPlacements.DashboardPanelMenu, extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
context: createExtensionContext(panel, dashboard), context: createExtensionContext(panel, dashboard),
}); });

View File

@ -1 +1 @@
export const MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN = 2; export const MAX_EXTENSIONS_PER_POINT = 2;

View File

@ -14,7 +14,7 @@ describe('createRegistry()', () => {
title: 'Link 1', title: 'Link 1',
description: 'Link 1 description', description: 'Link 1 description',
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
placement: placement1, extensionPointId: placement1,
configure: jest.fn().mockReturnValue({}), configure: jest.fn().mockReturnValue({}),
}; };
link2 = { link2 = {
@ -22,7 +22,7 @@ describe('createRegistry()', () => {
title: 'Link 2', title: 'Link 2',
description: 'Link 2 description', description: 'Link 2 description',
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
placement: placement2, extensionPointId: placement2,
configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), configure: jest.fn().mockImplementation((context) => ({ title: context?.title })),
}; };

View File

@ -1,14 +1,14 @@
import type { PluginPreloadResult } from '../pluginPreloader'; import type { PluginPreloadResult } from '../pluginPreloader';
import { MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN } from './constants'; import { MAX_EXTENSIONS_PER_POINT } from './constants';
import { PlacementsPerPlugin } from './placementsPerPlugin'; import { ExtensionsPerPlugin } from './extensionsPerPlugin';
import type { PluginExtensionRegistryItem, PluginExtensionRegistry } from './types'; import type { PluginExtensionRegistryItem, PluginExtensionRegistry } from './types';
import { deepFreeze, logWarning } from './utils'; import { deepFreeze, logWarning } from './utils';
import { isPluginExtensionConfigValid } from './validators'; import { isPluginExtensionConfigValid } from './validators';
export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloadResult[]): PluginExtensionRegistry { export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloadResult[]): PluginExtensionRegistry {
const registry: PluginExtensionRegistry = {}; const registry: PluginExtensionRegistry = {};
const placementsPerPlugin = new PlacementsPerPlugin(); const extensionsPerPlugin = new ExtensionsPerPlugin();
for (const { pluginId, extensionConfigs, error } of pluginPreloadResults) { for (const { pluginId, extensionConfigs, error } of pluginPreloadResults) {
if (error) { if (error) {
@ -17,11 +17,11 @@ export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloa
} }
for (const extensionConfig of extensionConfigs) { for (const extensionConfig of extensionConfigs) {
const { placement } = extensionConfig; const { extensionPointId } = extensionConfig;
if (!placementsPerPlugin.allowedToAdd(extensionConfig)) { if (!extensionsPerPlugin.allowedToAdd(extensionConfig)) {
logWarning( logWarning(
`"${pluginId}" plugin has reached the limit of ${MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN} for "${placement}", skip registering extension "${extensionConfig.title}".` `"${pluginId}" plugin has reached the limit of ${MAX_EXTENSIONS_PER_POINT} for "${extensionPointId}", skip registering extension "${extensionConfig.title}".`
); );
continue; continue;
} }
@ -37,10 +37,10 @@ export function createPluginExtensionRegistry(pluginPreloadResults: PluginPreloa
pluginId, pluginId,
}; };
if (!Array.isArray(registry[placement])) { if (!Array.isArray(registry[extensionPointId])) {
registry[placement] = [registryItem]; registry[extensionPointId] = [registryItem];
} else { } else {
registry[placement].push(registryItem); registry[extensionPointId].push(registryItem);
} }
} }
} }

View File

@ -0,0 +1,33 @@
import { PluginExtensionLinkConfig } from '@grafana/data';
import { MAX_EXTENSIONS_PER_POINT } from './constants';
export class ExtensionsPerPlugin {
private extensionsByExtensionPoint: Record<string, string[]> = {};
allowedToAdd({ extensionPointId, title }: PluginExtensionLinkConfig): boolean {
if (this.countByExtensionPoint(extensionPointId) >= MAX_EXTENSIONS_PER_POINT) {
return false;
}
this.addExtensionToExtensionPoint(extensionPointId, title);
return true;
}
addExtensionToExtensionPoint(extensionPointId: string, extensionTitle: string) {
if (!this.extensionsByExtensionPoint[extensionPointId]) {
this.extensionsByExtensionPoint[extensionPointId] = [];
}
this.extensionsByExtensionPoint[extensionPointId].push(extensionTitle);
}
countByExtensionPoint(extensionPointId: string) {
return this.extensionsByExtensionPoint[extensionPointId]?.length ?? 0;
}
getExtensionTitlesByExtensionPoint(extensionPointId: string) {
return this.extensionsByExtensionPoint[extensionPointId];
}
}

View File

@ -5,8 +5,8 @@ import { getPluginExtensions } from './getPluginExtensions';
import { assertPluginExtensionLink } from './validators'; import { assertPluginExtensionLink } from './validators';
describe('getPluginExtensions()', () => { describe('getPluginExtensions()', () => {
const placement1 = 'grafana/dashboard/panel/menu'; const extensionPoint1 = 'grafana/dashboard/panel/menu';
const placement2 = 'plugins/myorg-basic-app/start'; const extensionPoint2 = 'plugins/myorg-basic-app/start';
const pluginId = 'grafana-basic-app'; const pluginId = 'grafana-basic-app';
let link1: PluginExtensionLinkConfig, link2: PluginExtensionLinkConfig; let link1: PluginExtensionLinkConfig, link2: PluginExtensionLinkConfig;
@ -16,7 +16,7 @@ describe('getPluginExtensions()', () => {
title: 'Link 1', title: 'Link 1',
description: 'Link 1 description', description: 'Link 1 description',
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
placement: placement1, extensionPointId: extensionPoint1,
configure: jest.fn().mockReturnValue({}), configure: jest.fn().mockReturnValue({}),
}; };
link2 = { link2 = {
@ -24,7 +24,7 @@ describe('getPluginExtensions()', () => {
title: 'Link 2', title: 'Link 2',
description: 'Link 2 description', description: 'Link 2 description',
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
placement: placement2, extensionPointId: extensionPoint2,
configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), configure: jest.fn().mockImplementation((context) => ({ title: context?.title })),
}; };
@ -33,7 +33,7 @@ describe('getPluginExtensions()', () => {
test('should return the extensions for the given placement', () => { test('should return the extensions for the given placement', () => {
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement1 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
expect(extensions).toHaveLength(1); expect(extensions).toHaveLength(1);
expect(extensions[0]).toEqual( expect(extensions[0]).toEqual(
@ -49,7 +49,7 @@ describe('getPluginExtensions()', () => {
test('should return with an empty list if there are no extensions registered for a placement yet', () => { test('should return with an empty list if there are no extensions registered for a placement yet', () => {
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: 'placement-with-no-extensions' }); const { extensions } = getPluginExtensions({ registry, extensionPointId: 'placement-with-no-extensions' });
expect(extensions).toEqual([]); expect(extensions).toEqual([]);
}); });
@ -58,7 +58,7 @@ describe('getPluginExtensions()', () => {
const context = { title: 'New title from the context!' }; const context = { title: 'New title from the context!' };
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
getPluginExtensions({ registry, context, placement: placement2 }); getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
expect(link2.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1);
expect(link2.configure).toHaveBeenCalledWith(context); expect(link2.configure).toHaveBeenCalledWith(context);
@ -72,7 +72,7 @@ describe('getPluginExtensions()', () => {
})); }));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
const [extension] = extensions; const [extension] = extensions;
assertPluginExtensionLink(extension); assertPluginExtensionLink(extension);
@ -91,7 +91,7 @@ describe('getPluginExtensions()', () => {
})); }));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
expect(link2.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1);
expect(extensions).toHaveLength(0); expect(extensions).toHaveLength(0);
@ -99,7 +99,7 @@ describe('getPluginExtensions()', () => {
test('should pass a frozen copy of the context to the configure() function', () => { test('should pass a frozen copy of the context to the configure() function', () => {
const context = { title: 'New title from the context!' }; const context = { title: 'New title from the context!' };
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, context, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, context, extensionPointId: extensionPoint2 });
const [extension] = extensions; const [extension] = extensions;
const frozenContext = (link2.configure as jest.Mock).mock.calls[0][0]; const frozenContext = (link2.configure as jest.Mock).mock.calls[0][0];
@ -121,7 +121,7 @@ describe('getPluginExtensions()', () => {
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
expect(() => { expect(() => {
getPluginExtensions({ registry, placement: placement2 }); getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
}).not.toThrow(); }).not.toThrow();
expect(link2.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1);
@ -138,8 +138,8 @@ describe('getPluginExtensions()', () => {
})); }));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link1, link2] }]);
const { extensions: extensionsAtPlacement1 } = getPluginExtensions({ registry, placement: placement1 }); const { extensions: extensionsAtPlacement1 } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
const { extensions: extensionsAtPlacement2 } = getPluginExtensions({ registry, placement: placement2 }); const { extensions: extensionsAtPlacement2 } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
expect(extensionsAtPlacement1).toHaveLength(0); expect(extensionsAtPlacement1).toHaveLength(0);
expect(extensionsAtPlacement2).toHaveLength(0); expect(extensionsAtPlacement2).toHaveLength(0);
@ -158,7 +158,7 @@ describe('getPluginExtensions()', () => {
link2.configure = jest.fn().mockImplementation(() => overrides); link2.configure = jest.fn().mockImplementation(() => overrides);
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
expect(extensions).toHaveLength(0); expect(extensions).toHaveLength(0);
expect(link2.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1);
@ -169,7 +169,7 @@ describe('getPluginExtensions()', () => {
link2.configure = jest.fn().mockImplementation(() => Promise.resolve({})); link2.configure = jest.fn().mockImplementation(() => Promise.resolve({}));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
expect(extensions).toHaveLength(0); expect(extensions).toHaveLength(0);
expect(link2.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1);
@ -180,7 +180,7 @@ describe('getPluginExtensions()', () => {
link2.configure = jest.fn().mockImplementation(() => undefined); link2.configure = jest.fn().mockImplementation(() => undefined);
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
expect(extensions).toHaveLength(0); expect(extensions).toHaveLength(0);
expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged
@ -194,7 +194,7 @@ describe('getPluginExtensions()', () => {
const context = {}; const context = {};
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
const [extension] = extensions; const [extension] = extensions;
assertPluginExtensionLink(extension); assertPluginExtensionLink(extension);
@ -217,7 +217,7 @@ describe('getPluginExtensions()', () => {
link2.onClick = jest.fn().mockRejectedValue(new Error('testing')); link2.onClick = jest.fn().mockRejectedValue(new Error('testing'));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
const [extension] = extensions; const [extension] = extensions;
assertPluginExtensionLink(extension); assertPluginExtensionLink(extension);
@ -236,7 +236,7 @@ describe('getPluginExtensions()', () => {
}); });
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, placement: placement2 }); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
const [extension] = extensions; const [extension] = extensions;
assertPluginExtensionLink(extension); assertPluginExtensionLink(extension);

View File

@ -11,18 +11,18 @@ import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise
type GetExtensions = ({ type GetExtensions = ({
context, context,
placement, extensionPointId,
registry, registry,
}: { }: {
context?: object | Record<string | symbol, unknown>; context?: object | Record<string | symbol, unknown>;
placement: string; extensionPointId: string;
registry: PluginExtensionRegistry; registry: PluginExtensionRegistry;
}) => { extensions: PluginExtension[] }; }) => { extensions: PluginExtension[] };
// Returns with a list of plugin extensions for the given placement // Returns with a list of plugin extensions for the given extension point
export const getPluginExtensions: GetExtensions = ({ context, placement, registry }) => { export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, registry }) => {
const frozenContext = context ? deepFreeze(context) : {}; const frozenContext = context ? deepFreeze(context) : {};
const registryItems = registry[placement] ?? []; const registryItems = registry[extensionPointId] ?? [];
// We don't return the extensions separated by type, because in that case it would be much harder to define a sort-order for them. // We don't return the extensions separated by type, because in that case it would be much harder to define a sort-order for them.
const extensions: PluginExtension[] = []; const extensions: PluginExtension[] = [];

View File

@ -1,33 +0,0 @@
import { PluginExtensionLinkConfig } from '@grafana/data';
import { MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN } from './constants';
export class PlacementsPerPlugin {
private extensionsByPlacement: Record<string, string[]> = {};
allowedToAdd({ placement, title }: PluginExtensionLinkConfig): boolean {
if (this.countByPlacement(placement) >= MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN) {
return false;
}
this.addExtensionToPlacement(placement, title);
return true;
}
addExtensionToPlacement(placement: string, extensionTitle: string) {
if (!this.extensionsByPlacement[placement]) {
this.extensionsByPlacement[placement] = [];
}
this.extensionsByPlacement[placement].push(extensionTitle);
}
countByPlacement(placement: string) {
return this.extensionsByPlacement[placement]?.length ?? 0;
}
getExtensionTitlesByPlacement(placement: string) {
return this.extensionsByPlacement[placement];
}
}

View File

@ -104,7 +104,7 @@ export function deepFreeze(value?: object | Record<string | symbol, unknown> | u
} }
export function generateExtensionId(pluginId: string, extensionConfig: PluginExtensionConfig): string { export function generateExtensionId(pluginId: string, extensionConfig: PluginExtensionConfig): string {
const str = `${pluginId}${extensionConfig.placement}${extensionConfig.title}`; const str = `${pluginId}${extensionConfig.extensionPointId}${extensionConfig.title}`;
return Array.from(str) return Array.from(str)
.reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0) .reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0)

View File

@ -3,7 +3,7 @@ import { PluginExtension, PluginExtensionLinkConfig, PluginExtensionTypes } from
import { import {
assertConfigureIsValid, assertConfigureIsValid,
assertLinkPathIsValid, assertLinkPathIsValid,
assertPlacementIsValid, assertExtensionPointIdIsValid,
assertPluginExtensionLink, assertPluginExtensionLink,
assertStringProps, assertStringProps,
isPluginExtensionConfigValid, isPluginExtensionConfigValid,
@ -43,7 +43,7 @@ describe('Plugin Extension Validators', () => {
path: `/a/${pluginId}/overview`, path: `/a/${pluginId}/overview`,
title: 'My Plugin', title: 'My Plugin',
description: 'My Plugin Description', description: 'My Plugin Description',
placement: '...', extensionPointId: '...',
}; };
assertLinkPathIsValid(pluginId, extension.path); assertLinkPathIsValid(pluginId, extension.path);
@ -56,7 +56,7 @@ describe('Plugin Extension Validators', () => {
path: `/a/myorg-b-app/overview`, path: `/a/myorg-b-app/overview`,
title: 'My Plugin', title: 'My Plugin',
description: 'My Plugin Description', description: 'My Plugin Description',
placement: '...', extensionPointId: '...',
}; };
assertLinkPathIsValid('another-plugin-app', extension.path); assertLinkPathIsValid('another-plugin-app', extension.path);
@ -69,7 +69,7 @@ describe('Plugin Extension Validators', () => {
path: `/some-bad-path`, path: `/some-bad-path`,
title: 'My Plugin', title: 'My Plugin',
description: 'My Plugin Description', description: 'My Plugin Description',
placement: '...', extensionPointId: '...',
}; };
assertLinkPathIsValid('myorg-b-app', extension.path); assertLinkPathIsValid('myorg-b-app', extension.path);
@ -77,35 +77,35 @@ describe('Plugin Extension Validators', () => {
}); });
}); });
describe('assertPlacementIsValid()', () => { describe('assertExtensionPointIdIsValid()', () => {
it('should throw an error if the placement does not have the right prefix', () => { it('should throw an error if the extensionPointId does not have the right prefix', () => {
expect(() => { expect(() => {
assertPlacementIsValid({ assertExtensionPointIdIsValid({
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
path: '...', path: '...',
placement: 'some-bad-placement', extensionPointId: 'wrong-extension-point-id',
}); });
}).toThrowError(); }).toThrowError();
}); });
it('should NOT throw an error if the placement is correct', () => { it('should NOT throw an error if the extensionPointId is correct', () => {
expect(() => { expect(() => {
assertPlacementIsValid({ assertExtensionPointIdIsValid({
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
path: '...', path: '...',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
}); });
assertPlacementIsValid({ assertExtensionPointIdIsValid({
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
path: '...', path: '...',
placement: 'plugins/my-super-plugin/some-page/some-placement', extensionPointId: 'plugins/my-super-plugin/some-page/extension-point-a',
}); });
}).not.toThrowError(); }).not.toThrowError();
}); });
@ -117,7 +117,7 @@ describe('Plugin Extension Validators', () => {
assertConfigureIsValid({ assertConfigureIsValid({
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
} as PluginExtensionLinkConfig); } as PluginExtensionLinkConfig);
}).not.toThrowError(); }).not.toThrowError();
}); });
@ -127,7 +127,7 @@ describe('Plugin Extension Validators', () => {
assertConfigureIsValid({ assertConfigureIsValid({
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
configure: () => {}, configure: () => {},
} as PluginExtensionLinkConfig); } as PluginExtensionLinkConfig);
}).not.toThrowError(); }).not.toThrowError();
@ -140,7 +140,7 @@ describe('Plugin Extension Validators', () => {
{ {
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
handler: () => {}, handler: () => {},
configure: '() => {}', configure: '() => {}',
} as PluginExtensionLinkConfig } as PluginExtensionLinkConfig
@ -155,9 +155,9 @@ describe('Plugin Extension Validators', () => {
assertStringProps( assertStringProps(
{ {
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
}, },
['title', 'description', 'placement'] ['title', 'description', 'extensionPointId']
); );
}).toThrowError(); }).toThrowError();
}); });
@ -168,9 +168,9 @@ describe('Plugin Extension Validators', () => {
{ {
title: '', title: '',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
}, },
['title', 'description', 'placement'] ['title', 'description', 'extensionPointId']
); );
}).toThrowError(); }).toThrowError();
}); });
@ -181,9 +181,9 @@ describe('Plugin Extension Validators', () => {
{ {
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
}, },
['title', 'description', 'placement'] ['title', 'description', 'extensionPointId']
); );
}).not.toThrowError(); }).not.toThrowError();
}); });
@ -194,10 +194,10 @@ describe('Plugin Extension Validators', () => {
{ {
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
dontCare: '', dontCare: '',
}, },
['title', 'description', 'placement'] ['title', 'description', 'extensionPointId']
); );
}).not.toThrowError(); }).not.toThrowError();
}); });
@ -212,8 +212,8 @@ describe('Plugin Extension Validators', () => {
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement',
onClick: jest.fn(), onClick: jest.fn(),
extensionPointId: 'grafana/some-page/extension-point-a',
} as PluginExtensionLinkConfig) } as PluginExtensionLinkConfig)
).toBe(true); ).toBe(true);
@ -222,7 +222,7 @@ describe('Plugin Extension Validators', () => {
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
path: `/a/${pluginId}/page`, path: `/a/${pluginId}/page`,
} as PluginExtensionLinkConfig) } as PluginExtensionLinkConfig)
).toBe(true); ).toBe(true);
@ -239,7 +239,7 @@ describe('Plugin Extension Validators', () => {
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
path: '/administration/users', path: '/administration/users',
} as PluginExtensionLinkConfig) } as PluginExtensionLinkConfig)
).toBe(false); ).toBe(false);
@ -250,7 +250,7 @@ describe('Plugin Extension Validators', () => {
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
} as PluginExtensionLinkConfig) } as PluginExtensionLinkConfig)
).toBe(false); ).toBe(false);
@ -260,7 +260,7 @@ describe('Plugin Extension Validators', () => {
type: PluginExtensionTypes.link, type: PluginExtensionTypes.link,
title: '', title: '',
description: 'Description', description: 'Description',
placement: 'grafana/some-page/some-placement', extensionPointId: 'grafana/some-page/extension-point-a',
path: `/a/${pluginId}/page`, path: `/a/${pluginId}/page`,
} as PluginExtensionLinkConfig) } as PluginExtensionLinkConfig)
).toBe(false); ).toBe(false);

View File

@ -29,10 +29,10 @@ export function assertLinkPathIsValid(pluginId: string, path: string) {
} }
} }
export function assertPlacementIsValid(extension: PluginExtensionLinkConfig) { export function assertExtensionPointIdIsValid(extension: PluginExtensionLinkConfig) {
if (!isPlacementValid(extension)) { if (!isExtensionPointIdValid(extension)) {
throw new Error( throw new Error(
`Invalid extension "${extension.title}". The placement should start with either "grafana/" or "plugins/" (currently: "${extension.placement}"). Skipping the extension.` `Invalid extension "${extension.title}". The extensionPointId should start with either "grafana/" or "plugins/" (currently: "${extension.extensionPointId}"). Skipping the extension.`
); );
} }
} }
@ -65,8 +65,10 @@ export function isLinkPathValid(pluginId: string, path: string) {
return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`)); return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`));
} }
export function isPlacementValid(extension: PluginExtensionLinkConfig) { export function isExtensionPointIdValid(extension: PluginExtensionLinkConfig) {
return Boolean(extension.placement?.startsWith('grafana/') || extension.placement?.startsWith('plugins/')); return Boolean(
extension.extensionPointId?.startsWith('grafana/') || extension.extensionPointId?.startsWith('plugins/')
);
} }
export function isConfigureFnValid(extension: PluginExtensionLinkConfig) { export function isConfigureFnValid(extension: PluginExtensionLinkConfig) {
@ -79,8 +81,8 @@ export function isStringPropValid(prop: unknown) {
export function isPluginExtensionConfigValid(pluginId: string, extension: PluginExtensionLinkConfig): boolean { export function isPluginExtensionConfigValid(pluginId: string, extension: PluginExtensionLinkConfig): boolean {
try { try {
assertStringProps(extension, ['title', 'description', 'placement']); assertStringProps(extension, ['title', 'description', 'extensionPointId']);
assertPlacementIsValid(extension); assertExtensionPointIdIsValid(extension);
assertConfigureIsValid(extension); assertConfigureIsValid(extension);
if (isPluginExtensionLinkConfig(extension)) { if (isPluginExtensionLinkConfig(extension)) {

View File

@ -62,7 +62,7 @@ export const TestStuffPage = () => {
<Page navModel={{ node: node, main: node }}> <Page navModel={{ node: node, main: node }}>
<Page.Contents> <Page.Contents>
<HorizontalGroup> <HorizontalGroup>
<LinkToBasicApp placement="grafana/sandbox/testing" /> <LinkToBasicApp extensionPointId="grafana/sandbox/testing" />
</HorizontalGroup> </HorizontalGroup>
{data && ( {data && (
<AutoSizer style={{ width: '100%', height: '600px' }}> <AutoSizer style={{ width: '100%', height: '600px' }}>
@ -148,8 +148,8 @@ export function getDefaultState(): State {
}; };
} }
function LinkToBasicApp({ placement }: { placement: string }) { function LinkToBasicApp({ extensionPointId }: { extensionPointId: string }) {
const { extensions } = getPluginExtensions({ placement }); const { extensions } = getPluginExtensions({ extensionPointId });
if (extensions.length === 0) { if (extensions.length === 0) {
return null; return null;