From c1546541625fadde55abecc0ff7377bd54683d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 3 Dec 2025 11:32:11 +0100 Subject: [PATCH] chore: preload plugins from new api --- packages/grafana-data/package.json | 1 + packages/grafana-data/src/index.ts | 1 + packages/grafana-data/src/types/plugin.ts | 55 ++++++++++++++++++ packages/grafana-runtime/src/config.ts | 3 + public/app/app.ts | 17 ++++++ .../app/features/plugins/extensions/utils.tsx | 58 +++++++++++-------- .../app/features/plugins/pluginPreloader.ts | 19 +++--- yarn.lock | 8 +++ 8 files changed, 129 insertions(+), 33 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 59d3c58a042..2be0df41626 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -84,6 +84,7 @@ "xss": "^1.0.14" }, "devDependencies": { + "@grafana/plugin-types": "^0.0.48", "@grafana/scenes": "6.38.0", "@rollup/plugin-node-resolve": "16.0.1", "@testing-library/react": "16.3.0", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index ab6a55fb5ff..8cef9c73104 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -921,3 +921,4 @@ export { } from './rbac/rbac'; export { type UserStorage } from './types/userStorage'; +export { type PluginMetasResponse, type Spec } from './types/plugin'; diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 045dfdcee0b..f91c2196dbd 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -1,5 +1,7 @@ import { ComponentType } from 'react'; +import { PluginSchema } from '@grafana/plugin-types/plugin-schema'; + import { KeyValue } from './data'; import { IconName } from './icon'; @@ -266,3 +268,56 @@ export class GrafanaPlugin { this.meta = {} as T; } } +export interface PluginMetasResponse { + kind: string; + apiVersion: APIVersion; + metadata: StatusClass; + items: Item[]; +} + +export enum APIVersion { + PluginsGrafanaAppV0Alpha1 = 'plugins.grafana.app/v0alpha1', +} + +export interface Item { + kind: Kind; + apiVersion: APIVersion; + metadata: PurpleMetadata; + spec: Spec; + status: StatusClass; +} + +export enum Kind { + PluginMeta = 'PluginMeta', +} + +export interface PurpleMetadata { + name: string; + namespace: Namespace; +} + +export enum Namespace { + Default = 'default', +} + +export interface Spec { + pluginJson: PluginSchema; + module: Module; + baseURL: string; + signature: Signature; + angular: Angular; + translations?: PluginSchema['languages']; +} + +export interface Angular { + detected: boolean; +} + +export interface Module { + path: string; + loadingStrategy: PluginLoadingStrategy; +} + +export interface Signature { + status: PluginSignatureStatus; +} diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 17fbbef447a..3c185d5da27 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -26,6 +26,8 @@ import { UnifiedAlertingConfig, GrafanaConfig, CurrentUserDTO, + Spec, + PluginType, } from '@grafana/data'; /** @@ -266,6 +268,7 @@ export class GrafanaBootConfig { listScopesEndpoint = ''; openFeatureContext: Record = {}; + plugins: Record = { app: [], datasource: [], panel: [], renderer: [] }; constructor( options: BootData['settings'] & { diff --git a/public/app/app.ts b/public/app/app.ts index 8c9c5167e42..9e5e85de4db 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -11,12 +11,14 @@ import { createRoot } from 'react-dom/client'; import { locationUtil, monacoLanguageRegistry, + PluginMetasResponse, setLocale, setTimeZoneResolver, setWeekStart, standardEditorsRegistry, standardFieldConfigEditorRegistry, standardTransformersRegistry, + Spec, } from '@grafana/data'; import { DEFAULT_LANGUAGE } from '@grafana/i18n'; import { initializeI18n, loadNamespacedResources } from '@grafana/i18n/internal'; @@ -257,6 +259,21 @@ export class GrafanaApp { const skipAppPluginsPreload = config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render'; if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) { + const response = await backendSrv.get( + `/apis/plugins.grafana.app/v0alpha1/namespaces/${config.namespace}/pluginmetas` + ); + const plugins: Record = {}; + response.items.reduce((acc, curr) => { + if (!acc[curr.spec.pluginJson.type]) { + acc[curr.spec.pluginJson.type] = []; + } + + acc[curr.spec.pluginJson.type].push(curr.spec); + return acc; + }, plugins); + + updateConfig({ plugins }); + const appPluginsToAwait = getAppPluginsToAwait(); const appPluginsToPreload = getAppPluginsToPreload(); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 35feb659b6a..508a50038b0 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -15,10 +15,12 @@ import { urlUtil, PluginExtensionPoints, ExtensionInfo, + Spec, } from '@grafana/data'; -import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; +import { reportInteraction, config } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; import { appEvents } from 'app/core/app_events'; +import { getConfig } from 'app/core/config'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { CloseExtensionSidebarEvent, @@ -605,7 +607,7 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string, export const isGrafanaDevMode = () => config.buildInfo.env === 'development'; export const getAppPluginConfigs = (pluginIds: string[] = []) => - Object.values(config.apps).filter((app) => pluginIds.includes(app.id)); + getConfig().plugins.app.filter((app) => pluginIds.includes(app.pluginJson.id)); export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => { return exposedComponentId.split('/')[0]; @@ -615,13 +617,13 @@ export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) // (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.) export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => { - return Object.values(config.apps) - .filter( + return getConfig() + .plugins.app.filter( (app) => - app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) || - app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId)) + app.pluginJson.extensions?.addedLinks?.some((link) => link.targets.includes(extensionPointId)) || + app.pluginJson.extensions?.addedComponents?.some((component) => component.targets.includes(extensionPointId)) ) - .map((app) => app.id) + .map((app) => app.pluginJson.id) .reduce((acc: string[], id: string) => { return [...acc, id, ...getAppPluginDependencies(id)]; }, []); @@ -644,22 +646,26 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension return new Map( getExtensionPointPluginDependencies(extensionPointId) .map((pluginId) => { - const app = config.apps[pluginId]; + const app = getConfig().plugins.app.find((a) => a.pluginJson.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))) + (!app.pluginJson.extensions?.addedComponents?.some((component) => + component.targets.includes(extensionPointId) + ) && + !app.pluginJson.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)), + addedComponents: + app.pluginJson.extensions?.addedComponents?.filter((component) => + component.targets.includes(extensionPointId) + ) || [], + addedLinks: + app.pluginJson.extensions?.addedLinks?.filter((link) => link.targets.includes(extensionPointId)) || [], }, ] as const; }) @@ -681,7 +687,7 @@ export const getExposedComponentPluginDependencies = (exposedComponentId: string // metadata field. (For example the plugins that expose components that the app depends on.) // Heads up! This is a recursive function. export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => { - if (!config.apps[pluginId]) { + if (!getConfig().plugins.app.find((a) => a.pluginJson.id === pluginId)) { return []; } @@ -690,17 +696,17 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [ return []; } - const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map( - getAppPluginIdFromExposedComponentId - ); + const spec = getConfig().plugins.app.find((a) => a.pluginJson.id === pluginId); + const pluginIdDependencies = + spec?.pluginJson.dependencies?.extensions?.exposedComponents?.map(getAppPluginIdFromExposedComponentId) || []; return ( pluginIdDependencies - .reduce((acc, _pluginId) => { + ?.reduce((acc, _pluginId) => { return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])]; }, pluginIdDependencies) // We don't want the plugin to "depend on itself" - .filter((id) => id !== pluginId) + .filter((id) => id !== pluginId) || [] ); }; @@ -711,17 +717,19 @@ export const getAppPluginsToAwait = () => { 'cloud-home-app', ]; - return Object.values(config.apps).filter((app) => pluginIds.includes(app.id)); + const app = getConfig().plugins?.app; + console.log({ app }); + return getConfig().plugins?.app.filter((app) => pluginIds.includes(app.pluginJson.id)); }; // Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization. export const getAppPluginsToPreload = () => { // 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 = getExtensionPointPluginDependencies(PluginExtensionPoints.DashboardPanelMenu); - const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id); - const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id); + const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.pluginJson.id); + const isNotAwaited = (app: Spec) => !awaitedPluginIds.includes(app.pluginJson.id); - return Object.values(config.apps).filter((app) => { - return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id)); + return getConfig().plugins?.app.filter((app) => { + return isNotAwaited(app) && (app.pluginJson.preload || dashboardPanelMenuPluginIds.includes(app.pluginJson.id)); }); }; diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 85b7d262a9e..2580f2dcf5b 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -1,8 +1,8 @@ import type { - AppPluginConfig, PluginExtensionAddedLinkConfig, PluginExtensionExposedComponentConfig, PluginExtensionAddedComponentConfig, + Spec, } from '@grafana/data'; import { contextSrv } from 'app/core/services/context_srv'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; @@ -23,29 +23,32 @@ export const clearPreloadedPluginsCache = () => { preloadPromises.clear(); }; -export async function preloadPlugins(apps: AppPluginConfig[] = []) { +export async function preloadPlugins(apps: Spec[] = []) { // Create preload promises for each app, reusing existing promises if already loading const promises = apps.map((app) => { - if (!preloadPromises.has(app.id)) { - preloadPromises.set(app.id, preload(app)); + if (!preloadPromises.has(app.pluginJson.id)) { + preloadPromises.set(app.pluginJson.id, preload(app)); } - return preloadPromises.get(app.id)!; + return preloadPromises.get(app.pluginJson.id)!; }); await Promise.all(promises); } -async function preload(config: AppPluginConfig): Promise { +async function preload(config: Spec): Promise { const showErrorAlert = contextSrv.user.orgRole !== ''; try { - const meta = await getPluginSettings(config.id, { showErrorAlert }); + const meta = await getPluginSettings(config.pluginJson.id, { showErrorAlert }); await pluginImporter.importApp(meta); } catch (error) { if (!showErrorAlert) { return; } - console.error(`[Plugins] Failed to preload plugin: ${config.path} (version: ${config.version})`, error); + console.error( + `[Plugins] Failed to preload plugin: ${config.module.path} (version: ${config.pluginJson.info.version})`, + error + ); } } diff --git a/yarn.lock b/yarn.lock index 2022568eb8b..7064784e500 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3106,6 +3106,7 @@ __metadata: dependencies: "@braintree/sanitize-url": "npm:7.0.1" "@grafana/i18n": "npm:12.4.0-pre" + "@grafana/plugin-types": "npm:^0.0.48" "@grafana/scenes": "npm:6.38.0" "@grafana/schema": "npm:12.4.0-pre" "@leeoniya/ufuzzy": "npm:1.0.19" @@ -3458,6 +3459,13 @@ __metadata: languageName: node linkType: hard +"@grafana/plugin-types@npm:^0.0.48": + version: 0.0.48 + resolution: "@grafana/plugin-types@npm:0.0.48" + checksum: 10/28514a152a00fffb13c2fd7ad8fe0fbef5b2ef869db89f2ad6c74a7f09b0d47b5d5e46f4edc6ce1a7d4741f4f30dcebde5e688f27c2ca3ee5be4f41ece2f93a4 + languageName: node + linkType: hard + "@grafana/plugin-ui@npm:^0.11.0, @grafana/plugin-ui@npm:^0.11.1": version: 0.11.1 resolution: "@grafana/plugin-ui@npm:0.11.1"