grafana/public/app/features/plugins/extensions/reactivePluginExtensionRegistry.ts
Marcus Andersson 804c726413
PluginExtensions: Make the extensions registry reactive (#83085)
* feat: add a reactive extension registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: add hooks to work with the reactive registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: start using the reactive registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "command palette" extension point to use the hook

* feat: update the "alerting" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "explore" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "datasources config" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "panel menu" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "pyroscope datasource" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "user profile page" extension point to use the hooks

* chore: update betterer

* fix: update the hooks to not re-render unnecessarily

* chore: remove the old `createPluginExtensionRegistry` impementation

* chore: add "TODO" for `PanelMenuBehaviour` extension point

* feat: update the return value of the hooks to contain a `{ isLoading }` param

* tests: add more tests for the usePluginExtensions() hook

* fix: exclude the cloud-home-app from being non-awaited

* refactor: use uuidv4() for random ID generation (for the registry object)

* fix: linting issue

* feat: use the hooks for the new alerting extension point

* feat: use `useMemo()` for `AlertInstanceAction` extension point context

---------

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
2024-04-24 09:33:16 +02:00

80 lines
2.7 KiB
TypeScript

import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { PluginPreloadResult } from '../pluginPreloader';
import { PluginExtensionRegistry, PluginExtensionRegistryItem } from './types';
import { deepFreeze, logWarning } from './utils';
import { isPluginExtensionConfigValid } from './validators';
export class ReactivePluginExtensionsRegistry {
private resultSubject: Subject<PluginPreloadResult>;
private registrySubject: ReplaySubject<PluginExtensionRegistry>;
constructor() {
this.resultSubject = new Subject<PluginPreloadResult>();
// This is the subject that we expose.
// (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
this.registrySubject = new ReplaySubject<PluginExtensionRegistry>(1);
this.resultSubject
.pipe(
scan(resultsToRegistry, { id: '', extensions: {} }),
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
startWith({ id: '', extensions: {} }),
map((registry) => deepFreeze(registry))
)
// Emitting the new registry to `this.registrySubject`
.subscribe(this.registrySubject);
}
register(result: PluginPreloadResult): void {
this.resultSubject.next(result);
}
asObservable(): Observable<PluginExtensionRegistry> {
return this.registrySubject.asObservable();
}
getRegistry(): Promise<PluginExtensionRegistry> {
return firstValueFrom(this.asObservable());
}
}
function resultsToRegistry(registry: PluginExtensionRegistry, result: PluginPreloadResult): PluginExtensionRegistry {
const { pluginId, extensionConfigs, error } = result;
// TODO: We should probably move this section to where we load the plugin since this is only used
// to provide a log to the user.
if (error) {
logWarning(`"${pluginId}" plugin failed to load, skip registering its extensions.`);
return registry;
}
for (const extensionConfig of extensionConfigs) {
const { extensionPointId } = extensionConfig;
if (!extensionConfig || !isPluginExtensionConfigValid(pluginId, extensionConfig)) {
return registry;
}
let registryItem: PluginExtensionRegistryItem = {
config: extensionConfig,
// Additional meta information about the extension
pluginId,
};
if (!Array.isArray(registry.extensions[extensionPointId])) {
registry.extensions[extensionPointId] = [registryItem];
} else {
registry.extensions[extensionPointId].push(registryItem);
}
}
// Add a unique ID to the registry (the registry object itself is immutable)
registry.id = uuidv4();
return registry;
}