mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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>
80 lines
2.7 KiB
TypeScript
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;
|
|
}
|