From 7fd2476a129eca2b292ee175ffdb6d3de828cd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 5 Dec 2025 11:04:18 +0100 Subject: [PATCH] bootdata: decouples config.apps --- .github/CODEOWNERS | 1 + eslint.config.js | 36 +++++ packages/grafana-runtime/src/index.ts | 1 + .../grafana-runtime/src/internal/index.ts | 1 + .../grafana-runtime/src/services/plugins.ts | 37 +++++ public/app/app.ts | 8 ++ .../alerting/unified/api/onCallApi.test.ts | 8 +- .../ConfirmConvertModal.test.tsx | 4 +- .../unified/mocks/server/configure.ts | 7 +- .../unified/mocks/server/handlers/plugins.ts | 43 +++--- .../unified/rule-editor/clone.utils.test.tsx | 10 +- .../alerting/unified/utils/config.test.ts | 19 +-- .../features/alerting/unified/utils/config.ts | 4 +- .../alerting/unified/utils/rules.test.ts | 6 +- .../features/alerting/unified/utils/rules.ts | 4 +- .../AdvisorRedirectNotice.test.tsx | 46 ++++--- .../AdvisorRedirectNotice.tsx | 5 +- .../dashboard/components/GenAI/utils.test.ts | 7 +- .../dashboard/components/GenAI/utils.ts | 4 +- .../registry/AddedComponentsRegistry.test.ts | 23 ++-- .../registry/AddedFunctionsRegistry.test.ts | 23 ++-- .../registry/AddedLinksRegistry.test.ts | 23 ++-- .../ExposedComponentsRegistry.test.ts | 24 ++-- .../extensions/usePluginComponent.test.tsx | 11 +- .../extensions/usePluginComponents.test.tsx | 53 +++---- .../extensions/usePluginFunctions.test.tsx | 53 +++---- .../extensions/usePluginLinks.test.tsx | 51 +++---- .../plugins/extensions/utils.test.tsx | 83 +++++------ .../app/features/plugins/extensions/utils.tsx | 16 +-- .../plugins/extensions/validators.test.tsx | 129 +++++++++++++----- .../features/plugins/extensions/validators.ts | 10 +- .../features/plugins/sandbox/codeLoader.ts | 4 +- 32 files changed, 466 insertions(+), 288 deletions(-) create mode 100644 packages/grafana-runtime/src/services/plugins.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30bba5db0ff..c8d2b07edb0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -652,6 +652,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /packages/grafana-runtime/src/components/QueryEditorWithMigration* @grafana/plugins-platform-frontend @grafana/plugins-platform-backend /packages/grafana-runtime/src/config.ts @grafana/grafana-frontend-platform /packages/grafana-runtime/src/services/ @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/services/plugins.ts @grafana/plugins-platform-frontend /packages/grafana-runtime/src/services/pluginExtensions @grafana/plugins-platform-frontend /packages/grafana-runtime/src/services/CorrelationsService.ts @grafana/datapro /packages/grafana-runtime/src/services/LocationService.test.tsx @grafana/grafana-search-navigate-organise diff --git a/eslint.config.js b/eslint.config.js index bd1be26465a..786ef0af5fd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -575,6 +575,42 @@ module.exports = [ "Property[key.name='a11y'][value.type='ObjectExpression'] Property[key.name='test'][value.value='off']", message: 'Skipping a11y tests is not allowed. Please fix the component or story instead.', }, + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas() from @grafana/runtime instead', + }, + ], + }, + }, + { + files: [...commonTestIgnores], + ignores: [ + // FIXME: Remove once all enterprise issues are fixed - + // we don't have a suppressions file/approach for enterprise code yet + ...enterpriseIgnores, + ], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas() from @grafana/runtime instead', + }, + ], + }, + }, + { + files: [...enterpriseIgnores], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas() from @grafana/runtime instead', + }, ], }, }, diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 489f9157700..770e5b087ec 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -69,3 +69,4 @@ export { getCorrelationsService, setCorrelationsService, } from './services/CorrelationsService'; +export { getAppPluginMetas, getAppPluginMeta, type AppPluginMetas } from './services/plugins'; diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index aed6b86ebfb..8579a4ab51e 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -29,3 +29,4 @@ export { export { UserStorage } from '../utils/userStorage'; export { initOpenFeature, evaluateBooleanFlag } from './openFeature'; +export { initPluginMetas, setAppPluginMetas } from '../services/plugins'; diff --git a/packages/grafana-runtime/src/services/plugins.ts b/packages/grafana-runtime/src/services/plugins.ts new file mode 100644 index 00000000000..03a377c2b38 --- /dev/null +++ b/packages/grafana-runtime/src/services/plugins.ts @@ -0,0 +1,37 @@ +import { cloneDeep } from 'lodash'; + +import { AppPluginConfig } from '@grafana/data'; + +import { config } from '../config'; + +export type AppPluginMetas = Record; + +let apps: AppPluginMetas = {}; + +export async function initPluginMetas(): Promise { + if (config.featureToggles.useMTPlugins) { + // add loading app configs from MT API here + apps = {}; + return; + } + + // eslint-disable-next-line no-restricted-syntax + apps = config.apps; +} + +export function getAppPluginMetas(): AppPluginMetas { + return cloneDeep(apps); +} + +export function getAppPluginMeta(id: string): AppPluginConfig { + return getAppPluginMetas()[id]; +} + +export function setAppPluginMetas(override: AppPluginMetas) { + // We allow overriding apps in tests + if (override && process.env.NODE_ENV !== 'test') { + throw new Error('setAppPluginMetas() function can only be called from tests.'); + } + + apps = { ...override }; +} diff --git a/public/app/app.ts b/public/app/app.ts index 8c9c5167e42..76153fd663e 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -44,6 +44,7 @@ import { } from '@grafana/runtime'; import { initOpenFeature, + initPluginMetas, setGetObservablePluginComponents, setGetObservablePluginLinks, setPanelDataErrorView, @@ -176,6 +177,13 @@ export class GrafanaApp { // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); + try { + startMeasure('frontend_app_init_plugins'); + await initPluginMetas(); + } finally { + stopMeasure('frontend_app_init_plugins'); + } + setLocale(config.regionalFormat); setWeekStart(contextSrv.user.weekStart); setPanelRenderer(PanelRenderer); diff --git a/public/app/features/alerting/unified/api/onCallApi.test.ts b/public/app/features/alerting/unified/api/onCallApi.test.ts index a403f79b4a2..1c805165733 100644 --- a/public/app/features/alerting/unified/api/onCallApi.test.ts +++ b/public/app/features/alerting/unified/api/onCallApi.test.ts @@ -1,4 +1,4 @@ -import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; import { SupportedPlugin } from '../types/pluginBridges'; @@ -7,7 +7,7 @@ import { getProxyApiUrl } from './onCallApi'; describe('getProxyApiUrl', () => { it('should return URL with IRM plugin ID when IRM plugin is present', () => { - config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); expect(getProxyApiUrl('/alert_receive_channels/')).toBe( '/api/plugins/grafana-irm-app/resources/alert_receive_channels/' @@ -15,10 +15,10 @@ describe('getProxyApiUrl', () => { }); it('should return URL with OnCall plugin ID when IRM plugin is not present', () => { - config.apps = { + setAppPluginMetas({ [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }; + }); expect(getProxyApiUrl('/alert_receive_channels/')).toBe( '/api/plugins/grafana-oncall-app/resources/alert_receive_channels/' diff --git a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx index 2fe703a1347..59ad9c45d5f 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx @@ -1,4 +1,4 @@ -import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { pluginMeta, pluginMetaToPluginConfig } from '../../testSetup/plugins'; @@ -67,7 +67,7 @@ describe('filterRulerRulesConfig', () => { }; it('should filter by namespace', () => { - config.apps = { [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) }; + setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) }); const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1'); expect(filteredConfig).toEqual({ diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index 3e3043d9331..7a03cc28ea1 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -1,6 +1,7 @@ import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw'; -import { config } from '@grafana/runtime'; +import { getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import server from '@grafana/test-utils/server'; import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks'; import { @@ -214,7 +215,9 @@ export function setGrafanaPromRules(groups: GrafanaPromRuleGroupDTO[]) { /** Make a given plugin ID respond with a 404, as if it isn't installed at all */ export const removePlugin = (pluginId: string) => { - delete config.apps[pluginId]; + const apps = getAppPluginMetas(); + delete apps[pluginId]; + setAppPluginMetas(apps); server.use(getPluginMissingHandler(pluginId)); }; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts index e0a0142993f..5bb2abbe45f 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts @@ -1,7 +1,8 @@ import { HttpResponse, http } from 'msw'; import { PluginLoadingStrategy, PluginMeta } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { plugins } from 'app/features/alerting/unified/testSetup/plugins'; const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plugin with that id' }; @@ -12,28 +13,32 @@ const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plu */ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => { plugins.forEach(({ id, baseUrl, info, angular }) => { - config.apps[id] = { - id, - path: baseUrl, - preload: true, - version: info.version, - angular: angular ?? { detected: false, hideDeprecation: false }, - loadingStrategy: PluginLoadingStrategy.script, - extensions: { - addedLinks: [], - addedComponents: [], - extensionPoints: [], - exposedComponents: [], - addedFunctions: [], - }, - dependencies: { - grafanaVersion: '', - plugins: [], + const apps = getAppPluginMetas(); + setAppPluginMetas({ + ...apps, + [id]: { + id, + path: baseUrl, + preload: true, + version: info.version, + angular: angular ?? { detected: false, hideDeprecation: false }, + loadingStrategy: PluginLoadingStrategy.script, extensions: { + addedLinks: [], + addedComponents: [], + extensionPoints: [], exposedComponents: [], + addedFunctions: [], + }, + dependencies: { + grafanaVersion: '', + plugins: [], + extensions: { + exposedComponents: [], + }, }, }, - }; + }); }); return http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { diff --git a/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx b/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx index ff2f8f945db..2526ebd4c39 100644 --- a/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx @@ -1,4 +1,4 @@ -import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { RulerAlertingRuleDTO, @@ -137,9 +137,7 @@ describe('cloneRuleDefinition', () => { it('Should remove the origin label when cloning data source plugin-provided rules', () => { // Mock the plugin as installed - config.apps = { - [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]), - }; + setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) }); const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({ alert: 'slo-provider-alert', @@ -174,9 +172,7 @@ describe('cloneRuleDefinition', () => { }); it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => { - config.apps = { - [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]), - }; + setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) }); const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( { diff --git a/public/app/features/alerting/unified/utils/config.test.ts b/public/app/features/alerting/unified/utils/config.test.ts index 34a4734b55e..fe462d71210 100644 --- a/public/app/features/alerting/unified/utils/config.test.ts +++ b/public/app/features/alerting/unified/utils/config.test.ts @@ -1,4 +1,5 @@ import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; import { SupportedPlugin } from '../types/pluginBridges'; @@ -62,45 +63,45 @@ describe('checkEvaluationIntervalGlobalLimit', () => { describe('getIsIrmPluginPresent', () => { it('should return true when IRM plugin is present in config.apps', () => { - config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); expect(getIsIrmPluginPresent()).toBe(true); }); it('should return false when IRM plugin is not present in config.apps', () => { - config.apps = { + setAppPluginMetas({ [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }; + }); expect(getIsIrmPluginPresent()).toBe(false); }); }); describe('getIrmIfPresentOrIncidentPluginId', () => { it('should return IRM plugin ID when IRM plugin is present', () => { - config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm); }); it('should return Incident plugin ID when IRM plugin is not present', () => { - config.apps = { + setAppPluginMetas({ [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }; + }); expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident); }); }); describe('getIrmIfPresentOrOnCallPluginId', () => { it('should return IRM plugin ID when IRM plugin is present', () => { - config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm); }); it('should return OnCall plugin ID when IRM plugin is not present', () => { - config.apps = { + setAppPluginMetas({ [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }; + }); expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall); }); }); diff --git a/public/app/features/alerting/unified/utils/config.ts b/public/app/features/alerting/unified/utils/config.ts index 6a85614622a..3b0716d12f0 100644 --- a/public/app/features/alerting/unified/utils/config.ts +++ b/public/app/features/alerting/unified/utils/config.ts @@ -1,5 +1,5 @@ import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getAppPluginMetas } from '@grafana/runtime'; import { SupportedPlugin } from '../types/pluginBridges'; @@ -30,7 +30,7 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str } export function getIsIrmPluginPresent() { - return SupportedPlugin.Irm in config.apps; + return SupportedPlugin.Irm in getAppPluginMetas(); } export function getIrmIfPresentOrIncidentPluginId() { diff --git a/public/app/features/alerting/unified/utils/rules.test.ts b/public/app/features/alerting/unified/utils/rules.test.ts index 63c0cb62f04..dc5f0f15e87 100644 --- a/public/app/features/alerting/unified/utils/rules.test.ts +++ b/public/app/features/alerting/unified/utils/rules.test.ts @@ -1,5 +1,5 @@ import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { RuleGroupIdentifier } from 'app/types/unified-alerting'; import { @@ -42,7 +42,7 @@ describe('getRuleOrigin', () => { }); it('returns pluginId when origin label matches expected format and plugin is installed', () => { - config.apps = { + setAppPluginMetas({ installed_plugin: { id: 'installed_plugin', version: '', @@ -65,7 +65,7 @@ describe('getRuleOrigin', () => { }, }, }, - }; + }); const rule = mockPromAlertingRule({ labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' }, }); diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index e3d35972be5..efe78dfbaa1 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -1,7 +1,7 @@ import { capitalize } from 'lodash'; import { AlertState } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta } from '@grafana/runtime'; import { Alert, AlertingRule, @@ -273,7 +273,7 @@ export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): R } function isPluginInstalled(pluginId: string) { - return Boolean(config.apps[pluginId]); + return Boolean(getAppPluginMeta(pluginId)); } export function isPluginProvidedGroup(group: RulerRuleGroupDTO): boolean { diff --git a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx index 5d0a24797f9..68bd3ca2d13 100644 --- a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx +++ b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx @@ -3,12 +3,14 @@ import userEvent from '@testing-library/user-event'; import { PluginLoadingStrategy } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { contextSrv } from 'app/core/services/context_srv'; import { AdvisorRedirectNotice } from './AdvisorRedirectNotice'; const originalFeatureToggleValue = config.featureToggles.grafanaAdvisor; jest.mock('@grafana/runtime/internal', () => ({ + ...jest.requireActual('@grafana/runtime/internal'), UserStorage: jest.fn().mockImplementation(() => ({ getItem: jest.fn().mockResolvedValue('true'), setItem: jest.fn().mockResolvedValue(undefined), @@ -24,27 +26,29 @@ describe('AdvisorRedirectNotice', () => { afterEach(() => { jest.clearAllMocks(); config.featureToggles.grafanaAdvisor = originalFeatureToggleValue; - config.apps['grafana-advisor-app'] = { - id: 'grafana-advisor-app', - path: '/a/grafana-advisor-app', - version: '1.0.0', - preload: false, - angular: { detected: false, hideDeprecation: false }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaDependency: '*', - grafanaVersion: '*', - plugins: [], - extensions: { exposedComponents: [] }, + setAppPluginMetas({ + 'grafana-advisor-app': { + id: 'grafana-advisor-app', + path: '/a/grafana-advisor-app', + version: '1.0.0', + preload: false, + angular: { detected: false, hideDeprecation: false }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaDependency: '*', + grafanaVersion: '*', + plugins: [], + extensions: { exposedComponents: [] }, + }, + extensions: { + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + addedFunctions: [], + }, }, - extensions: { - addedLinks: [], - addedComponents: [], - exposedComponents: [], - extensionPoints: [], - addedFunctions: [], - }, - }; + }); }); it('should not render when user is not admin', async () => { @@ -60,7 +64,7 @@ describe('AdvisorRedirectNotice', () => { }); it('should not render when app is not installed', async () => { - delete config.apps['grafana-advisor-app']; + setAppPluginMetas({}); render(); expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); diff --git a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx index 3257daac941..e8eb8065db9 100644 --- a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx +++ b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; +import { config, getAppPluginMeta } from '@grafana/runtime'; import { UserStorage } from '@grafana/runtime/internal'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; @@ -28,7 +28,8 @@ export function AdvisorRedirectNotice() { const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; const [showNotice, setShowNotice] = useState(false); - const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!config.apps['grafana-advisor-app']; + const canUseAdvisor = + hasAdminRights && config.featureToggles.grafanaAdvisor && !!getAppPluginMeta('grafana-advisor-app'); useEffect(() => { if (canUseAdvisor) { diff --git a/public/app/features/dashboard/components/GenAI/utils.test.ts b/public/app/features/dashboard/components/GenAI/utils.test.ts index 80060af4c7d..a1f0f12810a 100644 --- a/public/app/features/dashboard/components/GenAI/utils.test.ts +++ b/public/app/features/dashboard/components/GenAI/utils.test.ts @@ -19,12 +19,7 @@ jest.mock('@grafana/llm', () => ({ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), - config: { - ...jest.requireActual('@grafana/runtime').config, - apps: { - 'grafana-llm-app': true, - }, - }, + getAppPluginMeta: (id: string) => ({ [id]: {} }), })); describe('getDashboardChanges', () => { diff --git a/public/app/features/dashboard/components/GenAI/utils.ts b/public/app/features/dashboard/components/GenAI/utils.ts index ea1a8fe425b..0d5c33a4422 100644 --- a/public/app/features/dashboard/components/GenAI/utils.ts +++ b/public/app/features/dashboard/components/GenAI/utils.ts @@ -1,7 +1,7 @@ import { pick } from 'lodash'; import { llm } from '@grafana/llm'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta } from '@grafana/runtime'; import { Panel } from '@grafana/schema'; import { DashboardModel } from '../../state/DashboardModel'; @@ -70,7 +70,7 @@ let llmHealthCheck: Promise | undefined; * @returns true if the LLM plugin is enabled. */ export async function isLLMPluginEnabled(): Promise { - if (!config.apps['grafana-llm-app']) { + if (!getAppPluginMeta('grafana-llm-app')) { return false; } diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index 98c0fb1781b..9fe21b37eae 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -2,7 +2,8 @@ import React from 'react'; import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -30,7 +31,7 @@ jest.mock('../logs/log', () => { }); describe('AddedComponentsRegistry', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -61,13 +62,11 @@ describe('AddedComponentsRegistry', () => { beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return empty registry when no extensions registered', async () => { @@ -450,7 +449,9 @@ describe('AddedComponentsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedComponents: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -499,7 +500,9 @@ describe('AddedComponentsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedComponents: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -525,7 +528,9 @@ describe('AddedComponentsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedComponents = [componentConfig]; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedComponents: [componentConfig] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts index 4c68a3a3eac..158ea38f874 100644 --- a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts @@ -1,7 +1,8 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -29,7 +30,7 @@ jest.mock('../logs/log', () => { }); describe('addedFunctionsRegistry', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -60,13 +61,11 @@ describe('addedFunctionsRegistry', () => { beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return empty registry when no extensions registered', async () => { @@ -642,7 +641,9 @@ describe('addedFunctionsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedFunctions: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -691,7 +692,9 @@ describe('addedFunctionsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedFunctions: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -717,7 +720,9 @@ describe('addedFunctionsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedFunctions = [fnConfig]; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedFunctions: [fnConfig] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts index 63ab6b55090..8c84fb3b6ea 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts @@ -1,7 +1,8 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -29,7 +30,7 @@ jest.mock('../logs/log', () => { }); describe('AddedLinksRegistry', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -60,13 +61,11 @@ describe('AddedLinksRegistry', () => { beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return empty registry when no extensions registered', async () => { @@ -624,7 +623,9 @@ describe('AddedLinksRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedLinks: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -675,7 +676,9 @@ describe('AddedLinksRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedLinks: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -702,7 +705,9 @@ describe('AddedLinksRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.addedLinks = [linkConfig]; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedLinks: [linkConfig] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts index 15af7e10308..122588ac55b 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts @@ -2,7 +2,8 @@ import React from 'react'; import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -30,7 +31,7 @@ jest.mock('../logs/log', () => { }); describe('ExposedComponentsRegistry', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -61,13 +62,11 @@ describe('ExposedComponentsRegistry', () => { beforeEach(() => { resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return empty registry when no exposed components have been registered', async () => { @@ -423,7 +422,9 @@ describe('ExposedComponentsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, exposedComponents: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -472,7 +473,9 @@ describe('ExposedComponentsRegistry', () => { }; // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = []; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, exposedComponents: [] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, @@ -497,8 +500,9 @@ describe('ExposedComponentsRegistry', () => { component: () => React.createElement('div', null, 'Hello World1'), }; - // Make sure that the meta-info is empty - config.apps[pluginId].extensions.exposedComponents = [componentConfig]; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, exposedComponents: [componentConfig] } }; + setAppPluginMetas({ [pluginId]: config }); registry.register({ pluginId, diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 44f35114b96..b2bce141eb9 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -2,7 +2,8 @@ import { act, render, renderHook, screen, waitFor } from '@testing-library/react import type { JSX } from 'react'; import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; @@ -51,7 +52,7 @@ describe('usePluginComponent()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const exposedComponentId = `${pluginId}/exposed-component/v1`; const exposedComponentConfig = { @@ -135,9 +136,7 @@ describe('usePluginComponent()', () => { }, }; - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -145,7 +144,7 @@ describe('usePluginComponent()', () => { }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return null if there are no component exposed for the id', () => { diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index f9c41cdc9b4..dc9750f5350 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -8,7 +8,8 @@ import { PluginMeta, PluginType, } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getAppPluginMeta } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import * as errors from './errors'; @@ -115,31 +116,33 @@ describe('usePluginComponents()', () => { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], + setAppPluginMetas({ + [pluginId]: { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, extensions: { + addedLinks: [], + addedComponents: [], + addedFunctions: [], exposedComponents: [], + extensionPoints: [], }, }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + }); wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -506,8 +509,10 @@ describe('usePluginComponents()', () => { component: () =>
Component
, }; - // The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedComponents = [componentConfig]; + // The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata. + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedComponents: [componentConfig] } }; + setAppPluginMetas({ [pluginId]: config }); wrapper = ({ children }: { children: React.ReactNode }) => ( { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], + setAppPluginMetas({ + [pluginId]: { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, extensions: { + addedLinks: [], + addedComponents: [], + addedFunctions: [], exposedComponents: [], + extensionPoints: [], }, }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + }); wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -328,8 +331,10 @@ describe('usePluginFunctions()', () => { fn: () => 'function1', }; - // The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedFunctions = [functionConfig]; + // The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata. + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedFunctions: [functionConfig] } }; + setAppPluginMetas({ [pluginId]: config }); wrapper = ({ children }: { children: React.ReactNode }) => ( { }, }; - config.apps[pluginId] = { - id: pluginId, - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], + setAppPluginMetas({ + [pluginId]: { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, extensions: { + addedLinks: [], + addedComponents: [], + addedFunctions: [], exposedComponents: [], + extensionPoints: [], }, }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; + }); wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -266,7 +269,9 @@ describe('usePluginLinks()', () => { }; // The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps). - config.apps[pluginId].extensions.addedLinks = [linkConfig]; + const plugin = getAppPluginMeta(pluginId); + const config = { ...plugin, extensions: { ...plugin.extensions, addedLinks: [linkConfig] } }; + setAppPluginMetas({ [pluginId]: config }); wrapper = ({ children }: { children: React.ReactNode }) => ( { }); describe('getAppPluginConfigs()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1024,11 +1025,11 @@ describe('Plugin Extensions / Utils', () => { }; afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); test('should return the app plugin configs based on the provided plugin ids', () => { - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1041,16 +1042,16 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-third-app', }, - }; + }); expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([ - config.apps['myorg-first-app'], - config.apps['myorg-third-app'], + getAppPluginMeta('myorg-first-app'), + getAppPluginMeta('myorg-third-app'), ]); }); test('should simply ignore the app plugin ids that do not belong to a config', () => { - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1063,9 +1064,9 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-third-app', }, - }; + }); - expect(getAppPluginConfigs(['myorg-first-app', 'unknown-app-id'])).toEqual([config.apps['myorg-first-app']]); + expect(getAppPluginConfigs(['myorg-first-app', 'unknown-app-id'])).toEqual([getAppPluginMeta('myorg-first-app')]); }); }); @@ -1076,7 +1077,7 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExtensionPointPluginDependencies()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1103,13 +1104,13 @@ describe('Plugin Extensions / Utils', () => { }; afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); test('should return the app plugin ids that register extensions to a link extension point', () => { const extensionPointId = 'myorg-first-app/link/v1'; - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1135,7 +1136,7 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-third-app', }, - }; + }); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); @@ -1145,7 +1146,7 @@ describe('Plugin Extensions / Utils', () => { test('should return the app plugin ids that register extensions to a component extension point', () => { const extensionPointId = 'myorg-first-app/component/v1'; - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1171,7 +1172,7 @@ describe('Plugin Extensions / Utils', () => { addedFunctions: [], }, }, - }; + }); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); @@ -1182,7 +1183,7 @@ describe('Plugin Extensions / Utils', () => { const extensionPointId = 'myorg-first-app/component/v1'; // None of the apps are extending the extension point - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1195,7 +1196,7 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-third-app', }, - }; + }); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); @@ -1205,7 +1206,7 @@ describe('Plugin Extensions / Utils', () => { test('should also return (recursively) the app plugin ids that the apps which extend the extension-point depend on', () => { const extensionPointId = 'myorg-first-app/component/v1'; - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1281,7 +1282,7 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-sixth-app', }, - }; + }); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); @@ -1290,7 +1291,7 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExposedComponentPluginDependencies()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1317,13 +1318,13 @@ describe('Plugin Extensions / Utils', () => { }; afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); test('should only return the app plugin id that exposes the component, if that component does not depend on anything', () => { const exposedComponentId = 'myorg-second-app/component/v1'; - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1348,7 +1349,7 @@ describe('Plugin Extensions / Utils', () => { ...genereicAppPluginConfig, id: 'myorg-third-app', }, - }; + }); const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId); @@ -1358,7 +1359,7 @@ describe('Plugin Extensions / Utils', () => { test('should also return the list of app plugin ids that the plugin - which exposes the component - is depending on', () => { const exposedComponentId = 'myorg-second-app/component/v1'; - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1427,7 +1428,7 @@ describe('Plugin Extensions / Utils', () => { addedFunctions: [], }, }, - }; + }); const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId); @@ -1436,7 +1437,7 @@ describe('Plugin Extensions / Utils', () => { }); describe('getAppPluginDependencies()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1463,11 +1464,11 @@ describe('Plugin Extensions / Utils', () => { }; afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); test('should not end up in an infinite loop if there are circular dependencies', () => { - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1492,7 +1493,7 @@ describe('Plugin Extensions / Utils', () => { }, }, }, - }; + }); const appPluginIds = getAppPluginDependencies('myorg-second-app'); @@ -1500,7 +1501,7 @@ describe('Plugin Extensions / Utils', () => { }); test('should not end up in an infinite loop if a plugin depends on itself', () => { - config.apps = { + setAppPluginMetas({ 'myorg-first-app': { ...genereicAppPluginConfig, id: 'myorg-first-app', @@ -1517,7 +1518,7 @@ describe('Plugin Extensions / Utils', () => { }, }, }, - }; + }); const appPluginIds = getAppPluginDependencies('myorg-second-app'); @@ -1526,7 +1527,7 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExtensionPointPluginMeta()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const mockExtensionPointId = 'test-extension-point'; const mockApp1: AppPluginConfig = { id: 'app1', @@ -1581,28 +1582,28 @@ describe('Plugin Extensions / Utils', () => { }; beforeEach(() => { - config.apps = {}; + setAppPluginMetas({}); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return empty map when no plugins have extensions for the point', () => { - config.apps = { + setAppPluginMetas({ app1: { ...mockApp1, extensions: { ...mockApp1.extensions, addedComponents: [], addedLinks: [] } }, app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } }, - }; + }); const result = getExtensionPointPluginMeta(mockExtensionPointId); expect(result.size).toBe(0); }); it('should return map with plugins that have components for the extension point', () => { - config.apps = { + setAppPluginMetas({ app1: mockApp1, app2: mockApp2, - }; + }); const result = getExtensionPointPluginMeta(mockExtensionPointId); @@ -1618,7 +1619,7 @@ describe('Plugin Extensions / Utils', () => { }); it('should filter out plugins that do not have any extensions for the point', () => { - config.apps = { + setAppPluginMetas({ app1: mockApp1, app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } }, app3: { @@ -1630,7 +1631,7 @@ describe('Plugin Extensions / Utils', () => { addedLinks: [{ title: 'Link 3', targets: ['other-point'] }], }, }, - }; + }); const result = getExtensionPointPluginMeta(mockExtensionPointId); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 9bc9171a49f..18868bbb93a 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -16,7 +16,7 @@ import { PluginExtensionPoints, ExtensionInfo, } from '@grafana/data'; -import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; +import { reportInteraction, config, AppPluginConfig, getAppPluginMetas, getAppPluginMeta } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; import { appEvents } from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; @@ -607,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)); + Object.values(getAppPluginMetas()).filter((app) => pluginIds.includes(app.id)); export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => { return exposedComponentId.split('/')[0]; @@ -617,7 +617,7 @@ 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) + return Object.values(getAppPluginMetas()) .filter( (app) => app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) || @@ -646,7 +646,7 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension return new Map( getExtensionPointPluginDependencies(extensionPointId) .map((pluginId) => { - const app = config.apps[pluginId]; + const app = getAppPluginMeta(pluginId); // if the plugin does not exist or does not expose any components or links to the extension point, return undefined if ( !app || @@ -683,7 +683,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 (!getAppPluginMeta(pluginId)) { return []; } @@ -692,7 +692,7 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [ return []; } - const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map( + const pluginIdDependencies = getAppPluginMeta(pluginId).dependencies.extensions.exposedComponents.map( getAppPluginIdFromExposedComponentId ); @@ -713,7 +713,7 @@ export const getAppPluginsToAwait = () => { 'cloud-home-app', ]; - return Object.values(config.apps).filter((app) => pluginIds.includes(app.id)); + return Object.values(getAppPluginMetas()).filter((app) => pluginIds.includes(app.id)); }; // Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization. @@ -723,7 +723,7 @@ export const getAppPluginsToPreload = () => { const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id); const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id); - return Object.values(config.apps).filter((app) => { + return Object.values(getAppPluginMetas()).filter((app) => { return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id)); }); }; diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index 513f8e881bc..2922b4f83be 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -7,7 +7,8 @@ import { PluginLoadingStrategy, PluginType, } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { getAppPluginMetas } from '@grafana/runtime'; +import { setAppPluginMetas } from '@grafana/runtime/internal'; import { createLogMock } from './logs/testUtils'; import { @@ -223,7 +224,7 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedLinkMetaInfoMissing()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -257,18 +258,20 @@ describe('Plugin Extension Validators', () => { }; beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); @@ -278,7 +281,7 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + setAppPluginMetas({}); const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); @@ -289,7 +292,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the link has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks = []; + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedLinks: [] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); @@ -302,7 +309,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the "targets" do not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedLinkMetaInfoMissing( pluginId, @@ -322,7 +333,11 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedLinkMetaInfoMissing( pluginId, @@ -340,12 +355,15 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with links with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedLinks.push(extensionConfig); const extensionConfig2 = { ...extensionConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.addedLinks.push(extensionConfig2); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig, extensionConfig2] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig2, log); @@ -355,7 +373,7 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -390,18 +408,20 @@ describe('Plugin Extension Validators', () => { }; beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); @@ -411,7 +431,7 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + setAppPluginMetas({}); const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); @@ -422,7 +442,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the Component has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents = []; + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedComponents: [] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); @@ -435,7 +459,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the "targets" do not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedComponentMetaInfoMissing( pluginId, @@ -454,7 +482,11 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedComponentMetaInfoMissing( pluginId, @@ -472,12 +504,15 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with components with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.addedComponents.push(extensionConfig); const extensionConfig2 = { ...extensionConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.addedComponents.push(extensionConfig2); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig, extensionConfig2] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig2, log); @@ -487,7 +522,7 @@ describe('Plugin Extension Validators', () => { }); describe('isExposedComponentMetaInfoMissing()', () => { - const originalApps = config.apps; + const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -522,18 +557,20 @@ describe('Plugin Extension Validators', () => { }; beforeEach(() => { - config.apps = { - [pluginId]: appPluginConfig, - }; + setAppPluginMetas({ [pluginId]: appPluginConfig }); }); afterEach(() => { - config.apps = originalApps; + setAppPluginMetas(originalApps); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, exposedComponents: [exposedComponentConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); @@ -543,7 +580,7 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the app config is not found', () => { const log = createLogMock(); - delete config.apps[pluginId]; + setAppPluginMetas({}); const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); @@ -554,7 +591,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the exposed component has no meta-info in the plugin.json', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents = []; + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, exposedComponents: [] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); @@ -567,7 +608,11 @@ describe('Plugin Extension Validators', () => { it('should return TRUE and log an error if the title does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, exposedComponents: [exposedComponentConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isExposedComponentMetaInfoMissing( pluginId, @@ -587,7 +632,11 @@ describe('Plugin Extension Validators', () => { it('should return FALSE and log a warning if the "description" does not match', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const config = { + ...appPluginConfig, + extensions: { ...appPluginConfig.extensions, exposedComponents: [exposedComponentConfig] }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isExposedComponentMetaInfoMissing( pluginId, @@ -605,12 +654,18 @@ describe('Plugin Extension Validators', () => { it('should return FALSE with components with the same title but different targets', () => { const log = createLogMock(); - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); const exposedComponentConfig2 = { ...exposedComponentConfig, targets: [PluginExtensionPoints.ExploreToolbarAction], }; - config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig2); + const config = { + ...appPluginConfig, + extensions: { + ...appPluginConfig.extensions, + exposedComponents: [exposedComponentConfig, exposedComponentConfig2], + }, + }; + setAppPluginMetas({ [pluginId]: config }); const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig2, log); diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index 07029a9a1e9..a87a93d5914 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -10,7 +10,7 @@ import { PluginExtensionPointPatterns, } from '@grafana/data'; import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal'; -import { config, isPluginExtensionLink } from '@grafana/runtime'; +import { getAppPluginMeta, isPluginExtensionLink } from '@grafana/runtime'; import * as errors from './errors'; import { ExtensionsLog } from './logs/log'; @@ -151,7 +151,7 @@ export const isAddedLinkMetaInfoMissing = ( log: ExtensionsLog ) => { const logPrefix = 'Could not register link extension. Reason:'; - const app = config.apps[pluginId]; + const app = getAppPluginMeta(pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null; if (!app) { @@ -183,7 +183,7 @@ export const isAddedFunctionMetaInfoMissing = ( log: ExtensionsLog ) => { const logPrefix = 'Could not register function extension. Reason:'; - const app = config.apps[pluginId]; + const app = getAppPluginMeta(pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null; if (!app) { @@ -215,7 +215,7 @@ export const isAddedComponentMetaInfoMissing = ( log: ExtensionsLog ) => { const logPrefix = 'Could not register component extension. Reason:'; - const app = config.apps[pluginId]; + const app = getAppPluginMeta(pluginId); const pluginJsonMetaInfo = app ? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title) : null; @@ -249,7 +249,7 @@ export const isExposedComponentMetaInfoMissing = ( log: ExtensionsLog ) => { const logPrefix = 'Could not register exposed component extension. Reason:'; - const app = config.apps[pluginId]; + const app = getAppPluginMeta(pluginId); const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null; if (!app) { diff --git a/public/app/features/plugins/sandbox/codeLoader.ts b/public/app/features/plugins/sandbox/codeLoader.ts index 67c3c4ec516..47162f3b931 100644 --- a/public/app/features/plugins/sandbox/codeLoader.ts +++ b/public/app/features/plugins/sandbox/codeLoader.ts @@ -1,5 +1,5 @@ import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getAppPluginMetas } from '@grafana/runtime'; import { transformPluginSourceForCDN } from '../cdn/utils'; import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache'; @@ -138,7 +138,7 @@ export function getPluginLoadData(pluginId: string): SandboxPluginMeta { //find it in apps //the information inside the apps object is more limited - for (const app of Object.values(config.apps)) { + for (const app of Object.values(getAppPluginMetas())) { if (app.id === pluginId) { return { id: pluginId,