mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 20:52:10 +08:00

* feat(plugins): remove global limit on extensions per placement * feat: add a way to limit extension per plugin at the getter * test(plugins): fix failing getPluginExtensions test * refactor(panelmenu): put back limit of 2 extensions per plugin * tests: add a test for checking all extensions are returned by default --------- Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
import {
|
|
type PluginExtension,
|
|
PluginExtensionTypes,
|
|
type PluginExtensionLink,
|
|
type PluginExtensionLinkConfig,
|
|
type PluginExtensionComponent,
|
|
} from '@grafana/data';
|
|
|
|
import type { PluginExtensionRegistry } from './types';
|
|
import {
|
|
isPluginExtensionLinkConfig,
|
|
getReadOnlyProxy,
|
|
logWarning,
|
|
generateExtensionId,
|
|
getEventHelpers,
|
|
isPluginExtensionComponentConfig,
|
|
} from './utils';
|
|
import {
|
|
assertIsReactComponent,
|
|
assertIsNotPromise,
|
|
assertLinkPathIsValid,
|
|
assertStringProps,
|
|
isPromise,
|
|
} from './validators';
|
|
|
|
type GetExtensions = ({
|
|
context,
|
|
extensionPointId,
|
|
limitPerPlugin,
|
|
registry,
|
|
}: {
|
|
context?: object | Record<string | symbol, unknown>;
|
|
extensionPointId: string;
|
|
limitPerPlugin?: number;
|
|
registry: PluginExtensionRegistry;
|
|
}) => { extensions: PluginExtension[] };
|
|
|
|
// Returns with a list of plugin extensions for the given extension point
|
|
export const getPluginExtensions: GetExtensions = ({ context, extensionPointId, limitPerPlugin, registry }) => {
|
|
const frozenContext = context ? getReadOnlyProxy(context) : {};
|
|
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.
|
|
const extensions: PluginExtension[] = [];
|
|
const extensionsByPlugin: Record<string, number> = {};
|
|
|
|
for (const registryItem of registryItems) {
|
|
try {
|
|
const extensionConfig = registryItem.config;
|
|
const { pluginId } = registryItem;
|
|
|
|
// Only limit if the `limitPerPlugin` is set
|
|
if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) {
|
|
continue;
|
|
}
|
|
|
|
if (extensionsByPlugin[pluginId] === undefined) {
|
|
extensionsByPlugin[pluginId] = 0;
|
|
}
|
|
|
|
// LINK
|
|
if (isPluginExtensionLinkConfig(extensionConfig)) {
|
|
// Run the configure() function with the current context, and apply the ovverides
|
|
const overrides = getLinkExtensionOverrides(registryItem.pluginId, extensionConfig, frozenContext);
|
|
|
|
// configure() returned an `undefined` -> hide the extension
|
|
if (extensionConfig.configure && overrides === undefined) {
|
|
continue;
|
|
}
|
|
|
|
const extension: PluginExtensionLink = {
|
|
id: generateExtensionId(registryItem.pluginId, extensionConfig),
|
|
type: PluginExtensionTypes.link,
|
|
pluginId: registryItem.pluginId,
|
|
onClick: getLinkExtensionOnClick(extensionConfig, frozenContext),
|
|
|
|
// Configurable properties
|
|
title: overrides?.title || extensionConfig.title,
|
|
description: overrides?.description || extensionConfig.description,
|
|
path: overrides?.path || extensionConfig.path,
|
|
};
|
|
|
|
extensions.push(extension);
|
|
extensionsByPlugin[pluginId] += 1;
|
|
}
|
|
|
|
// COMPONENT
|
|
if (isPluginExtensionComponentConfig(extensionConfig)) {
|
|
assertIsReactComponent(extensionConfig.component);
|
|
|
|
const extension: PluginExtensionComponent = {
|
|
id: generateExtensionId(registryItem.pluginId, extensionConfig),
|
|
type: PluginExtensionTypes.component,
|
|
pluginId: registryItem.pluginId,
|
|
|
|
title: extensionConfig.title,
|
|
description: extensionConfig.description,
|
|
component: extensionConfig.component,
|
|
};
|
|
|
|
extensions.push(extension);
|
|
extensionsByPlugin[pluginId] += 1;
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
logWarning(error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { extensions };
|
|
};
|
|
|
|
function getLinkExtensionOverrides(pluginId: string, config: PluginExtensionLinkConfig, context?: object) {
|
|
try {
|
|
const overrides = config.configure?.(context);
|
|
|
|
// Hiding the extension
|
|
if (overrides === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
let { title = config.title, description = config.description, path = config.path, ...rest } = overrides;
|
|
|
|
assertIsNotPromise(
|
|
overrides,
|
|
`The configure() function for "${config.title}" returned a promise, skipping updates.`
|
|
);
|
|
|
|
path && assertLinkPathIsValid(pluginId, path);
|
|
assertStringProps({ title, description }, ['title', 'description']);
|
|
|
|
if (Object.keys(rest).length > 0) {
|
|
throw new Error(
|
|
`Invalid extension "${config.title}". Trying to override not-allowed properties: ${Object.keys(rest).join(
|
|
', '
|
|
)}`
|
|
);
|
|
}
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
path,
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
logWarning(error.message);
|
|
}
|
|
|
|
// If there is an error, we hide the extension
|
|
// (This seems to be safest option in case the extension is doing something wrong.)
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function getLinkExtensionOnClick(
|
|
config: PluginExtensionLinkConfig,
|
|
context?: object
|
|
): ((event?: React.MouseEvent) => void) | undefined {
|
|
const { onClick } = config;
|
|
|
|
if (!onClick) {
|
|
return;
|
|
}
|
|
|
|
return function onClickExtensionLink(event?: React.MouseEvent) {
|
|
try {
|
|
const result = onClick(event, getEventHelpers(context));
|
|
|
|
if (isPromise(result)) {
|
|
result.catch((e) => {
|
|
if (e instanceof Error) {
|
|
logWarning(e.message);
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
logWarning(error.message);
|
|
}
|
|
}
|
|
};
|
|
}
|