mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Share plugin context with the component-type extensions (#78111)
feat: wrap component type extension with plugin context
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
|
||||
import React from 'react';
|
||||
|
||||
import { PluginExtensionComponentConfig, PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { createPluginExtensionRegistry } from './createPluginExtensionRegistry';
|
||||
@@ -16,8 +18,10 @@ jest.mock('@grafana/runtime', () => {
|
||||
describe('getPluginExtensions()', () => {
|
||||
const extensionPoint1 = 'grafana/dashboard/panel/menu';
|
||||
const extensionPoint2 = 'plugins/myorg-basic-app/start';
|
||||
const extensionPoint3 = 'grafana/datasources/config';
|
||||
const pluginId = 'grafana-basic-app';
|
||||
let link1: PluginExtensionLinkConfig, link2: PluginExtensionLinkConfig;
|
||||
// Sample extension configs that are used in the tests below
|
||||
let link1: PluginExtensionLinkConfig, link2: PluginExtensionLinkConfig, component1: PluginExtensionComponentConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
link1 = {
|
||||
@@ -36,6 +40,15 @@ describe('getPluginExtensions()', () => {
|
||||
extensionPointId: extensionPoint2,
|
||||
configure: jest.fn().mockImplementation((context) => ({ title: context?.title })),
|
||||
};
|
||||
component1 = {
|
||||
type: PluginExtensionTypes.component,
|
||||
title: 'Component 1',
|
||||
description: 'Component 1 description',
|
||||
extensionPointId: extensionPoint3,
|
||||
component: (context) => {
|
||||
return <div>Hello world!</div>;
|
||||
},
|
||||
};
|
||||
|
||||
global.console.warn = jest.fn();
|
||||
jest.mocked(reportInteraction).mockReset();
|
||||
@@ -409,4 +422,20 @@ describe('getPluginExtensions()', () => {
|
||||
category: extension.category,
|
||||
});
|
||||
});
|
||||
|
||||
test('should be possible to register and get component type extensions', () => {
|
||||
const extension = component1;
|
||||
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [extension] }]);
|
||||
const { extensions } = getPluginExtensions({ registry, extensionPointId: extension.extensionPointId });
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
pluginId,
|
||||
type: PluginExtensionTypes.component,
|
||||
title: extension.title,
|
||||
description: extension.description,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
generateExtensionId,
|
||||
getEventHelpers,
|
||||
isPluginExtensionComponentConfig,
|
||||
wrapWithPluginContext,
|
||||
} from './utils';
|
||||
import {
|
||||
assertIsReactComponent,
|
||||
@@ -101,7 +102,7 @@ export const getPluginExtensions: GetExtensions = ({ context, extensionPointId,
|
||||
|
||||
title: extensionConfig.title,
|
||||
description: extensionConfig.description,
|
||||
component: extensionConfig.component,
|
||||
component: wrapWithPluginContext(pluginId, extensionConfig.component),
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
|
||||
@@ -6,7 +6,14 @@ import { type PluginExtensionLinkConfig, PluginExtensionTypes, dateTime, usePlug
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { deepFreeze, isPluginExtensionLinkConfig, handleErrorsInFn, getReadOnlyProxy, getEventHelpers } from './utils';
|
||||
import {
|
||||
deepFreeze,
|
||||
isPluginExtensionLinkConfig,
|
||||
handleErrorsInFn,
|
||||
getReadOnlyProxy,
|
||||
getEventHelpers,
|
||||
wrapWithPluginContext,
|
||||
} from './utils';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
...jest.requireActual('app/features/plugins/pluginSettings'),
|
||||
@@ -436,4 +443,26 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapExtensionComponentWithContext()', () => {
|
||||
const ExampleComponent = () => {
|
||||
const { meta } = usePluginContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello Grafana!</h1> Version: {meta.info.version}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
it('should make the plugin context available for the wrapped component', async () => {
|
||||
const pluginId = 'grafana-worldmap-panel';
|
||||
const Component = wrapWithPluginContext(pluginId, ExampleComponent);
|
||||
|
||||
render(<Component />);
|
||||
|
||||
expect(await screen.findByText('Hello Grafana!')).toBeVisible();
|
||||
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isArray, isObject } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import {
|
||||
type PluginExtensionLinkConfig,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
isDateTime,
|
||||
dateTime,
|
||||
PluginContextProvider,
|
||||
PluginMeta,
|
||||
} from '@grafana/data';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -51,11 +51,10 @@ export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
|
||||
export function getEventHelpers(pluginId: string, context?: Readonly<object>): PluginExtensionEventHelpers {
|
||||
const openModal: PluginExtensionEventHelpers['openModal'] = async (options) => {
|
||||
const { title, body, width, height } = options;
|
||||
const pluginMeta = await getPluginSettings(pluginId);
|
||||
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: getModalWrapper({ title, body, width, height, pluginMeta }),
|
||||
component: wrapWithPluginContext<ModalWrapperProps>(pluginId, getModalWrapper({ title, body, width, height })),
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -67,6 +66,38 @@ type ModalWrapperProps = {
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
export const wrapWithPluginContext = <T,>(pluginId: string, Component: React.ComponentType<T>) => {
|
||||
const WrappedExtensionComponent = (props: T & React.JSX.IntrinsicAttributes) => {
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
value: pluginMeta,
|
||||
} = useAsync(() => getPluginSettings(pluginId, { showErrorAlert: false }));
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
logWarning(`Could not fetch plugin meta information for "${pluginId}", aborting. (${error.message})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pluginMeta) {
|
||||
logWarning(`Fetched plugin meta information is empty for "${pluginId}", aborting.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<Component {...props} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return WrappedExtensionComponent;
|
||||
};
|
||||
|
||||
// Wraps a component with a modal.
|
||||
// This way we can make sure that the modal is closable, and we also make the usage simpler.
|
||||
const getModalWrapper = ({
|
||||
@@ -76,17 +107,14 @@ const getModalWrapper = ({
|
||||
body: Body,
|
||||
width,
|
||||
height,
|
||||
pluginMeta,
|
||||
}: { pluginMeta: PluginMeta } & PluginExtensionOpenModalOptions) => {
|
||||
}: PluginExtensionOpenModalOptions) => {
|
||||
const className = css({ width, height });
|
||||
|
||||
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
|
||||
return (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<Modal title={title} className={className} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
||||
<Body onDismiss={onDismiss} />
|
||||
</Modal>
|
||||
</PluginContextProvider>
|
||||
<Modal title={title} className={className} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
||||
<Body onDismiss={onDismiss} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user