UI extensions: Refactor the registry and remove the "command" type (#65327)

* Wip

* Wip

* Wip

* Wip

* Wip
This commit is contained in:
Levente Balogh
2023-04-03 10:42:15 +02:00
committed by GitHub
parent bde77e4f79
commit 34f3878d26
36 changed files with 1475 additions and 1547 deletions
@@ -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;
}