import { AppPluginConfig, ExtensionInfo, PluginExtensionPoints } from '@grafana/data'; /** * Returns a list of app plugin configs that match the given plugin ids. * @param pluginIds - The list of plugin ids to filter by. * @param apps - The app plugin configs. * @returns A list of app plugin configs that match the given plugin ids. */ export function getAppPluginConfigsSync(pluginIds: string[] = [], apps: AppPluginConfig[]) { return apps.filter((app) => pluginIds.includes(app.id)); } /** * Returns the app plugin id from the exposed component id. * @param exposedComponentId - The id of the exposed component. * @returns The app plugin id. */ export function getAppPluginIdFromExposedComponentId(exposedComponentId: string) { return exposedComponentId.split('/')[0]; } /** * Returns a list of app plugin ids that are registering extensions to this extension point. * (These plugins are necessary to be loaded to use the extension point.) * (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.) * @param extensionPointId - The id of the extension point. * @param apps - The app plugin configs. * @returns A list of app plugin ids that are registering extensions to this extension point. */ export function getExtensionPointPluginDependenciesSync(extensionPointId: string, apps: AppPluginConfig[]): string[] { return apps .filter( (app) => app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) || app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId)) ) .map((app) => app.id) .reduce((acc: string[], id: string) => { return [...acc, id, ...getAppPluginDependenciesSync(id, apps)]; }, []); } export type ExtensionPointPluginMeta = Map< string, { readonly addedComponents: ExtensionInfo[]; readonly addedLinks: ExtensionInfo[] } >; /** * Returns a map of plugin ids and their addedComponents and addedLinks to the extension point. * @param extensionPointId - The id of the extension point. * @param apps - The app plugin configs. * @returns A map of plugin ids and their addedComponents and addedLinks to the extension point. */ export function getExtensionPointPluginMetaSync( extensionPointId: string, apps: AppPluginConfig[] ): ExtensionPointPluginMeta { return new Map( getExtensionPointPluginDependenciesSync(extensionPointId, apps) .map((pluginId) => { const app = apps.find((a) => a.id === pluginId); // if the plugin does not exist or does not expose any components or links to the extension point, return undefined if ( !app || (!app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId)) && !app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId))) ) { return undefined; } return [ pluginId, { addedComponents: app.extensions.addedComponents.filter((component) => component.targets.includes(extensionPointId) ), addedLinks: app.extensions.addedLinks.filter((link) => link.targets.includes(extensionPointId)), }, ] as const; }) .filter((c): c is NonNullable => c !== undefined) ); } /** * Returns a list of app plugin ids that are necessary to be loaded to use the exposed component. * (It is first the plugin that exposes the component, and then the ones that it depends on.) * @param exposedComponentId - The id of the exposed component. * @param apps - The app plugin configs. * @returns A list of app plugin ids that are necessary to be loaded to use the exposed component. */ export function getExposedComponentPluginDependenciesSync( exposedComponentId: string, apps: AppPluginConfig[] ): string[] { const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId); return [pluginId].reduce((acc: string[], pluginId: string) => { return [...acc, pluginId, ...getAppPluginDependenciesSync(pluginId, apps)]; }, []); } /** * Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions` * metadata field. (For example the plugins that expose components that the app depends on.) * Heads up! This is a recursive function. * @param pluginId - The id of the plugin. * @param visited - The list of plugin ids that have already been visited. * @param apps - The app plugin configs. * @returns A list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions` */ export function getAppPluginDependenciesSync( pluginId: string, apps: AppPluginConfig[], visited: string[] = [] ): string[] { const app = apps.find((a) => a.id === pluginId); if (!app) { return []; } // Prevent infinite recursion (it would happen if there is a circular dependency between app plugins) if (visited.includes(pluginId)) { return []; } const pluginIdDependencies = app.dependencies.extensions.exposedComponents.map(getAppPluginIdFromExposedComponentId); return ( pluginIdDependencies .reduce((acc, _pluginId) => { return [...acc, ...getAppPluginDependenciesSync(_pluginId, apps, [...visited, pluginId])]; }, pluginIdDependencies) // We don't want the plugin to "depend on itself" .filter((id) => id !== pluginId) ); } /** * Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization. * @param apps - The app plugin configs. * @returns An array of app plugin configs that has to be preloaded in parallel with the core Grafana initialization. */ export function getAppPluginsToPreloadSync(apps: AppPluginConfig[]): AppPluginConfig[] { // The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading. const dashboardPanelMenuPluginIds = getExtensionPointPluginDependenciesSync( PluginExtensionPoints.DashboardPanelMenu, apps ); // TODO(@MattIPv4): cloud-home-app is deprecated and should not be preloaded return apps.filter((app) => { return app.id !== 'cloud-home-app' && (app.preload || dashboardPanelMenuPluginIds.includes(app.id)); }); }