UI extensions: Refactor the registry and remove the "command" type (#65327)
* Wip * Wip * Wip * Wip * Wip
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
import { RawTimeRange, TimeZone } from '@grafana/data';
|
||||
|
||||
type Dashboard = {
|
||||
uid: string;
|
||||
title: string;
|
||||
tags: Readonly<Array<Readonly<string>>>;
|
||||
};
|
||||
|
||||
type Target = {
|
||||
pluginId: string;
|
||||
refId: string;
|
||||
};
|
||||
|
||||
export type PluginExtensionPanelContext = Readonly<{
|
||||
pluginId: string;
|
||||
id: number;
|
||||
title: string;
|
||||
timeRange: Readonly<RawTimeRange>;
|
||||
timeZone: TimeZone;
|
||||
dashboard: Readonly<Dashboard>;
|
||||
targets: Readonly<Array<Readonly<Target>>>;
|
||||
}>;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { assertPluginExtensionLink, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data';
|
||||
|
||||
import { getPluginExtensions } from './extensions';
|
||||
import { PluginExtensionRegistryItem, setPluginsExtensionRegistry } from './registry';
|
||||
|
||||
describe('getPluginExtensions', () => {
|
||||
describe('when getting extensions for placement', () => {
|
||||
const placement = 'grafana/dashboard/panel/menu';
|
||||
const pluginId = 'grafana-basic-app';
|
||||
|
||||
beforeAll(() => {
|
||||
setPluginsExtensionRegistry({
|
||||
[placement]: [
|
||||
createRegistryLinkItem({
|
||||
title: 'Declare incident',
|
||||
description: 'Declaring an incident in the app',
|
||||
path: `/a/${pluginId}/declare-incident`,
|
||||
key: 1,
|
||||
}),
|
||||
],
|
||||
'plugins/myorg-basic-app/start': [
|
||||
createRegistryLinkItem({
|
||||
title: 'Declare incident',
|
||||
description: 'Declaring an incident in the app',
|
||||
path: `/a/${pluginId}/declare-incident`,
|
||||
key: 2,
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return extensions with correct path', () => {
|
||||
const { extensions } = getPluginExtensions({ placement });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
|
||||
expect(extension.path).toBe(`/a/${pluginId}/declare-incident`);
|
||||
expect(extensions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return extensions with correct description', () => {
|
||||
const { extensions } = getPluginExtensions({ placement });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
|
||||
expect(extension.description).toBe('Declaring an incident in the app');
|
||||
expect(extensions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return extensions with correct title', () => {
|
||||
const { extensions } = getPluginExtensions({ placement });
|
||||
const [extension] = extensions;
|
||||
|
||||
assertPluginExtensionLink(extension);
|
||||
|
||||
expect(extension.title).toBe('Declare incident');
|
||||
expect(extensions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return an empty array when extensions can be found', () => {
|
||||
const { extensions } = getPluginExtensions({
|
||||
placement: 'plugins/not-installed-app/news',
|
||||
});
|
||||
|
||||
expect(extensions.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRegistryLinkItem(
|
||||
link: Omit<PluginExtensionLink, 'type'>
|
||||
): PluginExtensionRegistryItem<PluginExtensionLink> {
|
||||
return (context?: object) => ({
|
||||
...link,
|
||||
type: PluginExtensionTypes.link,
|
||||
});
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { type PluginExtension } from '@grafana/data';
|
||||
|
||||
import { getPluginsExtensionRegistry } from './registry';
|
||||
|
||||
export type PluginExtensionsOptions<T extends object> = {
|
||||
placement: string;
|
||||
context?: T;
|
||||
};
|
||||
|
||||
export type PluginExtensionsResult = {
|
||||
extensions: PluginExtension[];
|
||||
};
|
||||
|
||||
export function getPluginExtensions<T extends object = {}>(
|
||||
options: PluginExtensionsOptions<T>
|
||||
): PluginExtensionsResult {
|
||||
const { placement, context } = options;
|
||||
const registry = getPluginsExtensionRegistry();
|
||||
const configureFuncs = registry[placement] ?? [];
|
||||
|
||||
const extensions = configureFuncs.reduce<PluginExtension[]>((result, configure) => {
|
||||
const extension = configure(context);
|
||||
|
||||
// If the configure() function returns `undefined`, the extension is not displayed
|
||||
if (extension) {
|
||||
result.push(extension);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
extensions: extensions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { setPluginExtensionGetter, type GetPluginExtensions, getPluginExtensions } from './getPluginExtensions';
|
||||
|
||||
describe('Plugin Extensions / Get Plugin Extensions', () => {
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
test('should always return the same extension-getter function that was previously set', () => {
|
||||
const getter: GetPluginExtensions = jest.fn().mockReturnValue({ extensions: [] });
|
||||
|
||||
setPluginExtensionGetter(getter);
|
||||
getPluginExtensions({ placement: 'panel-menu' });
|
||||
|
||||
expect(getter).toHaveBeenCalledTimes(1);
|
||||
expect(getter).toHaveBeenCalledWith({ placement: 'panel-menu' });
|
||||
});
|
||||
|
||||
test('should throw an error when trying to redefine the app-wide extension-getter function', () => {
|
||||
// By default, NODE_ENV is set to 'test' in jest.config.js, which allows to override the registry in tests.
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const getter: GetPluginExtensions = () => ({ extensions: [] });
|
||||
|
||||
expect(() => {
|
||||
setPluginExtensionGetter(getter);
|
||||
setPluginExtensionGetter(getter);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('should throw an error when trying to access the extension-getter function before it was set', () => {
|
||||
// "Unsetting" the registry
|
||||
// @ts-ignore
|
||||
setPluginExtensionGetter(undefined);
|
||||
|
||||
expect(() => {
|
||||
getPluginExtensions({ placement: 'panel-menu' });
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PluginExtension } from '@grafana/data';
|
||||
|
||||
export type GetPluginExtensions = ({
|
||||
placement,
|
||||
context,
|
||||
}: {
|
||||
placement: string;
|
||||
context?: object | Record<string | symbol, unknown>;
|
||||
}) => {
|
||||
extensions: PluginExtension[];
|
||||
};
|
||||
|
||||
let singleton: GetPluginExtensions | undefined;
|
||||
|
||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||
// We allow overriding the registry in tests
|
||||
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setPluginExtensionGetter() function should only be called once, when Grafana is starting.');
|
||||
}
|
||||
singleton = instance;
|
||||
}
|
||||
|
||||
function getPluginExtensionGetter(): GetPluginExtensions {
|
||||
if (!singleton) {
|
||||
throw new Error('getPluginExtensionGetter() can only be used after the Grafana instance has started.');
|
||||
}
|
||||
return singleton;
|
||||
}
|
||||
|
||||
export const getPluginExtensions: GetPluginExtensions = (options) => getPluginExtensionGetter()(options);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { PluginExtension } from '@grafana/data';
|
||||
|
||||
export type PluginExtensionRegistryItem<T extends PluginExtension = PluginExtension, C extends object = object> = (
|
||||
context?: C
|
||||
) => T | undefined;
|
||||
|
||||
export type PluginExtensionRegistry = Record<string, PluginExtensionRegistryItem[]>;
|
||||
|
||||
let registry: PluginExtensionRegistry | undefined;
|
||||
|
||||
export function setPluginsExtensionRegistry(instance: PluginExtensionRegistry): void {
|
||||
if (registry && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setPluginsExtensionRegistry function should only be called once, when Grafana is starting.');
|
||||
}
|
||||
registry = instance;
|
||||
}
|
||||
|
||||
export function getPluginsExtensionRegistry(): PluginExtensionRegistry {
|
||||
if (!registry) {
|
||||
throw new Error('getPluginsExtensionRegistry can only be used after the Grafana instance has started.');
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PluginExtension, PluginExtensionTypes } from '@grafana/data';
|
||||
|
||||
import { isPluginExtensionLink } from './utils';
|
||||
|
||||
describe('Plugin Extensions / Utils', () => {
|
||||
describe('isPluginExtensionLink()', () => {
|
||||
test('should return TRUE if the object is a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
id: 'id',
|
||||
pluginId: 'plugin-id',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(true);
|
||||
});
|
||||
test('should return FALSE if the object is NOT a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
// @ts-ignore (Right now we only have a single type of extension)
|
||||
isPluginExtensionLink({
|
||||
type: 'unknown',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PluginExtension, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data';
|
||||
|
||||
export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink {
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return extension.type === PluginExtensionTypes.link && 'path' in extension;
|
||||
}
|
||||
Reference in New Issue
Block a user