From 84d22e176be8fbd291d57525d9b36c76d6f115a1 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 17 Feb 2025 14:50:35 +0100 Subject: [PATCH] PluginExtensions: Exposing registry meta for components returned via `usePluginComponents` (#100587) Exposing meta information from registry via Component.meta. --- packages/grafana-data/src/index.ts | 1 + .../src/types/pluginExtensions.ts | 2 + .../pluginExtensions/getPluginExtensions.ts | 3 +- .../extensions/usePluginComponents.test.tsx | 44 +++++++++++++++++++ .../extensions/usePluginComponents.tsx | 38 ++++++++++++++-- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 6aa49febd08..66cfac27a52 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -548,6 +548,7 @@ export { type PluginExtension, type PluginExtensionLink, type PluginExtensionComponent, + type PluginExtensionComponentMeta, type PluginExtensionConfig, type PluginExtensionFunction, type PluginExtensionLinkConfig, diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 5b10f80f143..ceff4e2cea5 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -32,6 +32,8 @@ export type PluginExtensionLink = PluginExtensionBase & { category?: string; }; +export type PluginExtensionComponentMeta = Omit; + export type PluginExtensionComponent = PluginExtensionBase & { type: PluginExtensionTypes.component; component: React.ComponentType; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index 2f70132d5fe..3d5de8ab3dc 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -3,6 +3,7 @@ import type { PluginExtensionLink, PluginExtensionComponent, PluginExtensionFunction, + PluginExtensionComponentMeta, } from '@grafana/data'; import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; @@ -42,7 +43,7 @@ export type UsePluginComponentResult = { }; export type UsePluginComponentsResult = { - components: Array>; + components: Array & { meta: PluginExtensionComponentMeta }>; isLoading: boolean; }; diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index fcb729fdd64..03aad063d53 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -159,6 +159,50 @@ describe('usePluginComponents()', () => { expect(screen.queryByText('Hello World3')).toBeNull(); }); + it('should return component with meta information attached to it', async () => { + registries.addedComponentsRegistry.register({ + pluginId, + configs: [ + { + targets: extensionPointId, + title: '1', + description: '1', + component: () =>
Hello World1
, + }, + { + targets: extensionPointId, + title: '2', + description: '2', + component: () =>
Hello World2
, + }, + { + targets: 'plugins/another-extension/v1', + title: '3', + description: '3', + component: () =>
Hello World3
, + }, + ], + }); + + const { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); + + expect(result.current.components.length).toBe(2); + expect(result.current.components[0].meta).toEqual({ + pluginId, + title: '1', + description: '1', + id: '-1921123020', + type: 'component', + }); + expect(result.current.components[1].meta).toEqual({ + pluginId, + title: '2', + description: '2', + id: '-1921123019', + type: 'component', + }); + }); + it('should dynamically update the extensions registered for a certain extension point', () => { let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index e583a69141d..0e59834db94 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useObservable } from 'react-use'; -import { usePluginContext } from '@grafana/data'; +import { PluginExtensionComponentMeta, PluginExtensionTypes, usePluginContext } from '@grafana/data'; import { UsePluginComponentOptions, UsePluginComponentsResult, @@ -10,8 +10,9 @@ import { import { useAddedComponentsRegistry } from './ExtensionRegistriesContext'; import * as errors from './errors'; import { log } from './logs/log'; +import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry'; import { useLoadAppPlugins } from './useLoadAppPlugins'; -import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils'; +import { generateExtensionId, getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils'; import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators'; // Returns an array of component extensions for the given extension point @@ -27,7 +28,7 @@ export function usePluginComponents({ return useMemo(() => { // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. const enableRestrictions = isGrafanaDevMode() && pluginContext; - const components: Array> = []; + const components: Array & { meta: PluginExtensionComponentMeta }> = []; const extensionsByPlugin: Record = {}; const pluginId = pluginContext?.meta.id ?? ''; const pointLog = log.child({ @@ -66,7 +67,12 @@ export function usePluginComponents({ extensionsByPlugin[pluginId] = 0; } - components.push(registryItem.component as React.ComponentType); + const component = createComponentWithMeta( + registryItem as AddedComponentRegistryItem, + extensionPointId + ); + + components.push(component); extensionsByPlugin[pluginId] += 1; } @@ -76,3 +82,27 @@ export function usePluginComponents({ }; }, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]); } + +function createComponentWithMeta( + registryItem: AddedComponentRegistryItem, + extensionPointId: string +): React.ComponentType & { meta: PluginExtensionComponentMeta } { + const { component: Component, ...config } = registryItem; + function ComponentWithMeta(props: Props) { + return ; + } + + ComponentWithMeta.displayName = Component.displayName; + ComponentWithMeta.defaultProps = Component.defaultProps; + ComponentWithMeta.propTypes = Component.propTypes; + ComponentWithMeta.contextTypes = Component.contextTypes; + ComponentWithMeta.meta = { + pluginId: config.pluginId, + title: config.title ?? '', + description: config.description ?? '', + id: generateExtensionId(config.pluginId, extensionPointId, config.title), + type: PluginExtensionTypes.component, + } satisfies PluginExtensionComponentMeta; + + return ComponentWithMeta; +}