Plugin Extensions: Add prop types to component extensions (#84295)

* feat: make it possible to specify prop types for component extensions

* Update packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts

* chore: adapted test case

* chore: update betterer

* feat: update types for configureComponentExtension()

* fix: remove type specifics for `configureExtensionComponent`

* Update betterer config

---------

Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com>
Co-authored-by: Darren Janeczek <darren.janeczek@grafana.com>
This commit is contained in:
Levente Balogh
2024-03-18 15:14:51 +02:00
committed by GitHub
co-authored by Darren Janeczek Darren Janeczek
parent fbb6ae35e7
commit 155e38edfe
7 changed files with 38 additions and 22 deletions
+3 -3
View File
@@ -765,6 +765,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
],
"packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -2947,9 +2950,6 @@ exports[`better eslint`] = {
"public/app/features/datasources/components/DataSourceTypeCardList.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/datasources/components/EditDataSource.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/datasources/components/picker/DataSourceCard.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
+1 -3
View File
@@ -111,9 +111,7 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
return this;
}
configureExtensionComponent<Context extends object>(
extension: Omit<PluginExtensionComponentConfig<Context>, 'type'>
) {
configureExtensionComponent<Props = {}>(extension: Omit<PluginExtensionComponentConfig<Props>, 'type'>) {
this._extensionConfigs.push({
...extension,
type: PluginExtensionTypes.component,
@@ -32,9 +32,9 @@ export type PluginExtensionLink = PluginExtensionBase & {
category?: string;
};
export type PluginExtensionComponent = PluginExtensionBase & {
export type PluginExtensionComponent<Props = {}> = PluginExtensionBase & {
type: PluginExtensionTypes.component;
component: React.ComponentType;
component: React.ComponentType<Props>;
};
export type PluginExtension = PluginExtensionLink | PluginExtensionComponent;
@@ -77,16 +77,14 @@ export type PluginExtensionLinkConfig<Context extends object = object> = {
category?: string;
};
export type PluginExtensionComponentConfig<Context extends object = object> = {
export type PluginExtensionComponentConfig<Props = {}> = {
type: PluginExtensionTypes.component;
title: string;
description: string;
// The React component that will be rendered as the extension
// (This component receives the context as a prop when it is rendered. You can just return `null` from the component to hide for certain contexts)
component: React.ComponentType<{
context?: Context;
}>;
// (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.)
component: React.ComponentType<Props>;
// The unique identifier of the Extension Point
// (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
@@ -41,10 +41,15 @@ export const getPluginLinkExtensions: GetPluginExtensions<PluginExtensionLink> =
};
};
export const getPluginComponentExtensions: GetPluginExtensions<PluginExtensionComponent> = (options) => {
// This getter doesn't support the `context` option (contextual information can be passed in as component props)
export const getPluginComponentExtensions = <Props = {}>(options: {
extensionPointId: string;
limitPerPlugin?: number;
}): { extensions: Array<PluginExtensionComponent<Props>> } => {
const { extensions } = getPluginExtensions(options);
const componentExtensions = extensions.filter(isPluginExtensionComponent) as Array<PluginExtensionComponent<Props>>;
return {
extensions: extensions.filter(isPluginExtensionComponent),
extensions: componentExtensions,
};
};
@@ -139,7 +139,9 @@ export function EditDataSourceView({
const extensions = useMemo(() => {
const allowedPluginIds = ['grafana-pdc-app', 'grafana-auth-app'];
const extensionPointId = PluginExtensionPoints.DataSourceConfig;
const { extensions } = getPluginComponentExtensions({ extensionPointId });
const { extensions } = getPluginComponentExtensions<{
context: PluginExtensionDataSourceConfigContext<DataSourceJsonData>;
}>({ extensionPointId });
return extensions.filter((e) => allowedPluginIds.includes(e.pluginId));
}, []);
@@ -202,9 +204,7 @@ export function EditDataSourceView({
{/* Extension point */}
{extensions.map((extension) => {
const Component = extension.component as React.ComponentType<{
context: PluginExtensionDataSourceConfigContext<DataSourceJsonData>;
}>;
const Component = extension.component;
return (
<div key={extension.id}>
@@ -445,12 +445,18 @@ describe('Plugin Extensions / Utils', () => {
});
describe('wrapExtensionComponentWithContext()', () => {
const ExampleComponent = () => {
type ExampleComponentProps = {
audience?: string;
};
const ExampleComponent = (props: ExampleComponentProps) => {
const { meta } = usePluginContext();
const audience = props.audience || 'Grafana';
return (
<div>
<h1>Hello Grafana!</h1> Version: {meta.info.version}
<h1>Hello {audience}!</h1> Version: {meta.info.version}
</div>
);
};
@@ -464,5 +470,15 @@ describe('Plugin Extensions / Utils', () => {
expect(await screen.findByText('Hello Grafana!')).toBeVisible();
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
});
it('should pass the properties into the wrapped component', async () => {
const pluginId = 'grafana-worldmap-panel';
const Component = wrapWithPluginContext(pluginId, ExampleComponent);
render(<Component audience="folks" />);
expect(await screen.findByText('Hello folks!')).toBeVisible();
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
});
});
});
@@ -79,7 +79,6 @@ export function UserProfileEditPage({
const extensionComponents = useMemo(() => {
const { extensions } = getPluginComponentExtensions({
extensionPointId: PluginExtensionPoints.UserProfileTab,
context: {},
});
return extensions;