Plugins: Share plugin context with the component-type extensions (#78111)

feat: wrap component type extension with plugin context
pull/78209/head
Levente Balogh 2 years ago committed by GitHub
parent 018e002505
commit 12c245720c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      public/app/features/plugins/extensions/getPluginExtensions.test.tsx
  2. 3
      public/app/features/plugins/extensions/getPluginExtensions.ts
  3. 31
      public/app/features/plugins/extensions/utils.test.tsx
  4. 48
      public/app/features/plugins/extensions/utils.tsx

@ -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>
);
};

Loading…
Cancel
Save