mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
44ccd73d46
commit
1380fa54d6
@ -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,
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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[];
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export const MAX_EXTENSIONS_PER_PLACEMENT_PER_PLUGIN = 2;
|
export const MAX_EXTENSIONS_PER_POINT = 2;
|
||||||
|
@ -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 })),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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[] = [];
|
||||||
|
|
||||||
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user