Skip to content

Commit 134467f

Browse files
Plugin Extensions: Introduce new registry for exposed components (grafana#91748)
* refactor app plugin internals * add base registry and exposed components registry * refactor usePluginComponent hook * change type name * fix hook * remove comments * fix broken tests * add more tests * remove link and component related changes * use right id format * add title prop * remove comments * rename registry * make exportedComponentsConfigs required * fix broken test * cleanup tests * fix prop name * remove capability related code * rename exported to exposed * refactor(extensions): make registry types generic * Update public/app/features/plugins/extensions/registry/ExportedComponentsRegistry.test.ts Co-authored-by: Levente Balogh <[email protected]> * fix levitate error --------- Co-authored-by: Levente Balogh <[email protected]>
1 parent 4a753dd commit 134467f

15 files changed

+575
-93
lines changed

packages/grafana-data/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ export {
554554
type PluginExtensionDataSourceConfigContext,
555555
type PluginExtensionCommandPaletteContext,
556556
type PluginExtensionOpenModalOptions,
557+
type PluginExposedComponentConfig,
557558
} from './types/pluginExtensions';
558559
export {
559560
type ScopeDashboardBindingSpec,

packages/grafana-data/src/types/app.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type PluginExtensionLinkConfig,
88
PluginExtensionTypes,
99
PluginExtensionComponentConfig,
10+
PluginExposedComponentConfig,
1011
PluginExtensionConfig,
1112
} from './pluginExtensions';
1213

@@ -56,6 +57,7 @@ export interface AppPluginMeta<T extends KeyValue = KeyValue> extends PluginMeta
5657
}
5758

5859
export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
60+
private _exposedComponentConfigs: PluginExposedComponentConfig[] = [];
5961
private _extensionConfigs: PluginExtensionConfig[] = [];
6062

6163
// Content under: /a/${plugin-id}/*
@@ -98,6 +100,10 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
98100
}
99101
}
100102

103+
get exposedComponentConfigs() {
104+
return this._exposedComponentConfigs;
105+
}
106+
101107
get extensionConfigs() {
102108
return this._extensionConfigs;
103109
}
@@ -142,16 +148,8 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
142148
return this;
143149
}
144150

145-
exposeComponent<Props = {}>(
146-
componentConfig: { id: string } & Omit<PluginExtensionComponentConfig<Props>, 'type' | 'extensionPointId'>
147-
) {
148-
const { id, ...extension } = componentConfig;
149-
150-
this._extensionConfigs.push({
151-
...extension,
152-
extensionPointId: `capabilities/${id}`,
153-
type: PluginExtensionTypes.component,
154-
} as PluginExtensionComponentConfig);
151+
exposeComponent<Props = {}>(componentConfig: PluginExposedComponentConfig<Props>) {
152+
this._exposedComponentConfigs.push(componentConfig as PluginExposedComponentConfig);
155153

156154
return this;
157155
}
@@ -165,7 +163,6 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
165163

166164
return this;
167165
}
168-
169166
/** @deprecated Use .addComponent() instead */
170167
configureExtensionComponent<Props = {}>(extension: Omit<PluginExtensionComponentConfig<Props>, 'type'>) {
171168
this.addComponent({

packages/grafana-data/src/types/pluginExtensions.ts

+23
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ export type PluginExtensionComponentConfig<Props = {}> = {
9595
extensionPointId: string;
9696
};
9797

98+
export type PluginExposedComponentConfig<Props = {}> = {
99+
/**
100+
* The unique identifier of the component
101+
* Shoud be in the format of `<pluginId>/<componentName>/<componentVersion>`. e.g. `myorg-todo-app/todo-list/v1`
102+
*/
103+
id: string;
104+
105+
/**
106+
* The title of the component
107+
*/
108+
title: string;
109+
110+
/**
111+
* A short description of the component
112+
*/
113+
description: string;
114+
115+
/**
116+
* The React component that will be exposed to other plugins
117+
*/
118+
component: React.ComponentType<Props>;
119+
};
120+
98121
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
99122

100123
export type PluginExtensionOpenModalOptions = {

public/app/app.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { DatasourceSrv } from './features/plugins/datasource_srv';
8585
import { getCoreExtensionConfigurations } from './features/plugins/extensions/getCoreExtensionConfigurations';
8686
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
8787
import { ReactivePluginExtensionsRegistry } from './features/plugins/extensions/reactivePluginExtensionRegistry';
88+
import { ExposedComponentsRegistry } from './features/plugins/extensions/registry/ExposedComponentsRegistry';
8889
import { createUsePluginComponent } from './features/plugins/extensions/usePluginComponent';
8990
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
9091
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
@@ -213,22 +214,30 @@ export class GrafanaApp {
213214
extensionsRegistry.register({
214215
pluginId: 'grafana',
215216
extensionConfigs: getCoreExtensionConfigurations(),
217+
exposedComponentConfigs: [],
216218
});
217219

220+
const exposedComponentsRegistry = new ExposedComponentsRegistry();
221+
218222
if (contextSrv.user.orgRole !== '') {
219223
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
220224
// TODO: remove the following exception once the issue mentioned above is fixed.
221225
const awaitedAppPluginIds = ['cloud-home-app'];
222226
const awaitedAppPlugins = Object.values(config.apps).filter((app) => awaitedAppPluginIds.includes(app.id));
223227
const appPlugins = Object.values(config.apps).filter((app) => !awaitedAppPluginIds.includes(app.id));
224228

225-
preloadPlugins(appPlugins, extensionsRegistry);
226-
await preloadPlugins(awaitedAppPlugins, extensionsRegistry, 'frontend_awaited_plugins_preload');
229+
preloadPlugins(appPlugins, extensionsRegistry, exposedComponentsRegistry);
230+
await preloadPlugins(
231+
awaitedAppPlugins,
232+
extensionsRegistry,
233+
exposedComponentsRegistry,
234+
'frontend_awaited_plugins_preload'
235+
);
227236
}
228237

229238
setPluginExtensionGetter(createPluginExtensionsGetter(extensionsRegistry));
230239
setPluginExtensionsHook(createUsePluginExtensions(extensionsRegistry));
231-
setPluginComponentHook(createUsePluginComponent(extensionsRegistry));
240+
setPluginComponentHook(createUsePluginComponent(exposedComponentsRegistry));
232241

233242
// initialize chrome service
234243
const queryParams = locationService.getSearchObject();

public/app/features/plugins/extensions/getPluginExtensions.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function createPluginExtensionRegistry(preloadResults: Array<{ pluginId: string;
2222
registry.register({
2323
pluginId,
2424
extensionConfigs,
25+
exposedComponentConfigs: [],
2526
});
2627
}
2728

public/app/features/plugins/extensions/reactivePluginExtensionRegistry.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ describe('createPluginExtensionsRegistry', () => {
3939
configure: jest.fn().mockReturnValue({}),
4040
},
4141
],
42+
exposedComponentConfigs: [],
4243
});
4344

4445
const registry = await reactiveRegistry.getRegistry();
@@ -64,6 +65,7 @@ describe('createPluginExtensionsRegistry', () => {
6465
configure: jest.fn().mockReturnValue({}),
6566
},
6667
],
68+
exposedComponentConfigs: [],
6769
});
6870

6971
const registry1 = await reactiveRegistry.getRegistry();
@@ -83,6 +85,7 @@ describe('createPluginExtensionsRegistry', () => {
8385
configure: jest.fn().mockReturnValue({}),
8486
},
8587
],
88+
exposedComponentConfigs: [],
8689
});
8790

8891
const registry2 = await reactiveRegistry.getRegistry();
@@ -116,6 +119,7 @@ describe('createPluginExtensionsRegistry', () => {
116119
configure: jest.fn().mockImplementation((context) => ({ title: context?.title })),
117120
},
118121
],
122+
exposedComponentConfigs: [],
119123
});
120124

121125
const registry = await reactiveRegistry.getRegistry();
@@ -168,6 +172,7 @@ describe('createPluginExtensionsRegistry', () => {
168172
configure: jest.fn().mockReturnValue({}),
169173
},
170174
],
175+
exposedComponentConfigs: [],
171176
});
172177

173178
const registry1 = await reactiveRegistry.getRegistry();
@@ -201,6 +206,7 @@ describe('createPluginExtensionsRegistry', () => {
201206
configure: jest.fn().mockReturnValue({}),
202207
},
203208
],
209+
exposedComponentConfigs: [],
204210
});
205211

206212
const registry2 = await reactiveRegistry.getRegistry();
@@ -251,6 +257,7 @@ describe('createPluginExtensionsRegistry', () => {
251257
configure: jest.fn().mockReturnValue({}),
252258
},
253259
],
260+
exposedComponentConfigs: [],
254261
});
255262

256263
const registry1 = await reactiveRegistry.getRegistry();
@@ -284,6 +291,7 @@ describe('createPluginExtensionsRegistry', () => {
284291
configure: jest.fn().mockReturnValue({}),
285292
},
286293
],
294+
exposedComponentConfigs: [],
287295
});
288296

289297
const registry2 = await reactiveRegistry.getRegistry();
@@ -335,6 +343,7 @@ describe('createPluginExtensionsRegistry', () => {
335343
configure: jest.fn().mockReturnValue({}),
336344
},
337345
],
346+
exposedComponentConfigs: [],
338347
});
339348

340349
// Register extensions to a different extension point
@@ -350,6 +359,7 @@ describe('createPluginExtensionsRegistry', () => {
350359
configure: jest.fn().mockReturnValue({}),
351360
},
352361
],
362+
exposedComponentConfigs: [],
353363
});
354364

355365
const registry2 = await reactiveRegistry.getRegistry();
@@ -399,6 +409,7 @@ describe('createPluginExtensionsRegistry', () => {
399409
configure: jest.fn().mockReturnValue({}),
400410
},
401411
],
412+
exposedComponentConfigs: [],
402413
});
403414

404415
// Register extensions to a different extension point
@@ -414,6 +425,7 @@ describe('createPluginExtensionsRegistry', () => {
414425
configure: jest.fn().mockReturnValue({}),
415426
},
416427
],
428+
exposedComponentConfigs: [],
417429
});
418430

419431
const registry2 = await reactiveRegistry.getRegistry();
@@ -469,6 +481,7 @@ describe('createPluginExtensionsRegistry', () => {
469481
configure: jest.fn().mockReturnValue({}),
470482
},
471483
],
484+
exposedComponentConfigs: [],
472485
});
473486

474487
expect(subscribeCallback).toHaveBeenCalledTimes(2);
@@ -486,6 +499,7 @@ describe('createPluginExtensionsRegistry', () => {
486499
configure: jest.fn().mockReturnValue({}),
487500
},
488501
],
502+
exposedComponentConfigs: [],
489503
});
490504

491505
expect(subscribeCallback).toHaveBeenCalledTimes(3);
@@ -538,6 +552,7 @@ describe('createPluginExtensionsRegistry', () => {
538552
configure: jest.fn().mockReturnValue({}),
539553
},
540554
],
555+
exposedComponentConfigs: [],
541556
});
542557

543558
observable.subscribe(subscribeCallback);
@@ -581,6 +596,7 @@ describe('createPluginExtensionsRegistry', () => {
581596
configure: jest.fn().mockReturnValue({}),
582597
},
583598
],
599+
exposedComponentConfigs: [],
584600
});
585601

586602
expect(consoleWarn).toHaveBeenCalled();
@@ -640,6 +656,7 @@ describe('createPluginExtensionsRegistry', () => {
640656
configure: jest.fn().mockReturnValue({}),
641657
},
642658
],
659+
exposedComponentConfigs: [],
643660
});
644661

645662
expect(consoleWarn).toHaveBeenCalled();
@@ -669,6 +686,7 @@ describe('createPluginExtensionsRegistry', () => {
669686
configure: jest.fn().mockReturnValue({}),
670687
},
671688
],
689+
exposedComponentConfigs: [],
672690
});
673691

674692
expect(consoleWarn).toHaveBeenCalled();

public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts

+2-22
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
44
import { PluginPreloadResult } from '../pluginPreloader';
55

66
import { PluginExtensionRegistry, PluginExtensionRegistryItem } from './types';
7-
import { deepFreeze, isPluginCapability, logWarning } from './utils';
7+
import { deepFreeze, logWarning } from './utils';
88
import { isPluginExtensionConfigValid } from './validators';
99

1010
export class ReactivePluginExtensionsRegistry {
@@ -54,21 +54,6 @@ function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPrel
5454
for (const extensionConfig of extensionConfigs) {
5555
const { extensionPointId } = extensionConfig;
5656

57-
// Change the extension point id for capabilities
58-
if (isPluginCapability(extensionConfig)) {
59-
const regex = /capabilities\/([a-zA-Z0-9_.\-\/]+)$/;
60-
const match = regex.exec(extensionPointId);
61-
62-
if (!match) {
63-
logWarning(
64-
`"${pluginId}" plugin has an invalid capability ID: ${extensionPointId.replace('capabilities/', '')} (It must be a string)`
65-
);
66-
continue;
67-
}
68-
69-
extensionConfig.extensionPointId = `capabilities/${match[1]}`;
70-
}
71-
7257
// Check if the config is valid
7358
if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) {
7459
return registry;
@@ -81,12 +66,7 @@ function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPrel
8166
pluginId,
8267
};
8368

84-
// Capability (only a single value per identifier, can be overriden)
85-
if (isPluginCapability(extensionConfig)) {
86-
registry.extensions[extensionPointId] = [registryItem];
87-
}
88-
// Extension (multiple extensions per extension point identifier)
89-
else if (!Array.isArray(registry.extensions[extensionPointId])) {
69+
if (!Array.isArray(registry.extensions[extensionPointId])) {
9070
registry.extensions[extensionPointId] = [registryItem];
9171
} else {
9272
registry.extensions[extensionPointId].push(registryItem);

0 commit comments

Comments
 (0)