bootdata: decouples config.apps

This commit is contained in:
Hugo Häggmark
2025-12-08 06:08:57 +01:00
parent 78b1ae4f27
commit 7fd2476a12
32 changed files with 466 additions and 288 deletions
+1
View File
@@ -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/components/QueryEditorWithMigration* @grafana/plugins-platform-frontend @grafana/plugins-platform-backend
/packages/grafana-runtime/src/config.ts @grafana/grafana-frontend-platform /packages/grafana-runtime/src/config.ts @grafana/grafana-frontend-platform
/packages/grafana-runtime/src/services/ @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/pluginExtensions @grafana/plugins-platform-frontend
/packages/grafana-runtime/src/services/CorrelationsService.ts @grafana/datapro /packages/grafana-runtime/src/services/CorrelationsService.ts @grafana/datapro
/packages/grafana-runtime/src/services/LocationService.test.tsx @grafana/grafana-search-navigate-organise /packages/grafana-runtime/src/services/LocationService.test.tsx @grafana/grafana-search-navigate-organise
+36
View File
@@ -575,6 +575,42 @@ module.exports = [
"Property[key.name='a11y'][value.type='ObjectExpression'] Property[key.name='test'][value.value='off']", "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.', 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',
},
], ],
}, },
}, },
+1
View File
@@ -69,3 +69,4 @@ export {
getCorrelationsService, getCorrelationsService,
setCorrelationsService, setCorrelationsService,
} from './services/CorrelationsService'; } from './services/CorrelationsService';
export { getAppPluginMetas, getAppPluginMeta, type AppPluginMetas } from './services/plugins';
@@ -29,3 +29,4 @@ export {
export { UserStorage } from '../utils/userStorage'; export { UserStorage } from '../utils/userStorage';
export { initOpenFeature, evaluateBooleanFlag } from './openFeature'; export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
export { initPluginMetas, setAppPluginMetas } from '../services/plugins';
@@ -0,0 +1,37 @@
import { cloneDeep } from 'lodash';
import { AppPluginConfig } from '@grafana/data';
import { config } from '../config';
export type AppPluginMetas = Record<string, AppPluginConfig>;
let apps: AppPluginMetas = {};
export async function initPluginMetas(): Promise<void> {
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 };
}
+8
View File
@@ -44,6 +44,7 @@ import {
} from '@grafana/runtime'; } from '@grafana/runtime';
import { import {
initOpenFeature, initOpenFeature,
initPluginMetas,
setGetObservablePluginComponents, setGetObservablePluginComponents,
setGetObservablePluginLinks, setGetObservablePluginLinks,
setPanelDataErrorView, setPanelDataErrorView,
@@ -176,6 +177,13 @@ export class GrafanaApp {
// This needs to be done after the `initEchoSrv` since it is being used under the hood. // This needs to be done after the `initEchoSrv` since it is being used under the hood.
startMeasure('frontend_app_init'); startMeasure('frontend_app_init');
try {
startMeasure('frontend_app_init_plugins');
await initPluginMetas();
} finally {
stopMeasure('frontend_app_init_plugins');
}
setLocale(config.regionalFormat); setLocale(config.regionalFormat);
setWeekStart(contextSrv.user.weekStart); setWeekStart(contextSrv.user.weekStart);
setPanelRenderer(PanelRenderer); setPanelRenderer(PanelRenderer);
@@ -1,4 +1,4 @@
import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal';
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
import { SupportedPlugin } from '../types/pluginBridges'; import { SupportedPlugin } from '../types/pluginBridges';
@@ -7,7 +7,7 @@ import { getProxyApiUrl } from './onCallApi';
describe('getProxyApiUrl', () => { describe('getProxyApiUrl', () => {
it('should return URL with IRM plugin ID when IRM plugin is present', () => { 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( expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
'/api/plugins/grafana-irm-app/resources/alert_receive_channels/' '/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', () => { it('should return URL with OnCall plugin ID when IRM plugin is not present', () => {
config.apps = { setAppPluginMetas({
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
}; });
expect(getProxyApiUrl('/alert_receive_channels/')).toBe( expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
'/api/plugins/grafana-oncall-app/resources/alert_receive_channels/' '/api/plugins/grafana-oncall-app/resources/alert_receive_channels/'
@@ -1,4 +1,4 @@
import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal';
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { pluginMeta, pluginMetaToPluginConfig } from '../../testSetup/plugins'; import { pluginMeta, pluginMetaToPluginConfig } from '../../testSetup/plugins';
@@ -67,7 +67,7 @@ describe('filterRulerRulesConfig', () => {
}; };
it('should filter by namespace', () => { 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'); const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1');
expect(filteredConfig).toEqual({ expect(filteredConfig).toEqual({
@@ -1,6 +1,7 @@
import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw'; 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 server from '@grafana/test-utils/server';
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks'; import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
import { 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 */ /** Make a given plugin ID respond with a 404, as if it isn't installed at all */
export const removePlugin = (pluginId: string) => { export const removePlugin = (pluginId: string) => {
delete config.apps[pluginId]; const apps = getAppPluginMetas();
delete apps[pluginId];
setAppPluginMetas(apps);
server.use(getPluginMissingHandler(pluginId)); server.use(getPluginMissingHandler(pluginId));
}; };
@@ -1,7 +1,8 @@
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { PluginLoadingStrategy, PluginMeta } from '@grafana/data'; 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'; import { plugins } from 'app/features/alerting/unified/testSetup/plugins';
const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plugin with that id' }; 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) => { export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
plugins.forEach(({ id, baseUrl, info, angular }) => { plugins.forEach(({ id, baseUrl, info, angular }) => {
config.apps[id] = { const apps = getAppPluginMetas();
id, setAppPluginMetas({
path: baseUrl, ...apps,
preload: true, [id]: {
version: info.version, id,
angular: angular ?? { detected: false, hideDeprecation: false }, path: baseUrl,
loadingStrategy: PluginLoadingStrategy.script, preload: true,
extensions: { version: info.version,
addedLinks: [], angular: angular ?? { detected: false, hideDeprecation: false },
addedComponents: [], loadingStrategy: PluginLoadingStrategy.script,
extensionPoints: [],
exposedComponents: [],
addedFunctions: [],
},
dependencies: {
grafanaVersion: '',
plugins: [],
extensions: { extensions: {
addedLinks: [],
addedComponents: [],
extensionPoints: [],
exposedComponents: [], exposedComponents: [],
addedFunctions: [],
},
dependencies: {
grafanaVersion: '',
plugins: [],
extensions: {
exposedComponents: [],
},
}, },
}, },
}; });
}); });
return http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { return http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => {
@@ -1,4 +1,4 @@
import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
import { import {
RulerAlertingRuleDTO, RulerAlertingRuleDTO,
@@ -137,9 +137,7 @@ describe('cloneRuleDefinition', () => {
it('Should remove the origin label when cloning data source plugin-provided rules', () => { it('Should remove the origin label when cloning data source plugin-provided rules', () => {
// Mock the plugin as installed // Mock the plugin as installed
config.apps = { setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) });
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
};
const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({ const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({
alert: 'slo-provider-alert', alert: 'slo-provider-alert',
@@ -174,9 +172,7 @@ describe('cloneRuleDefinition', () => {
}); });
it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => { it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => {
config.apps = { setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) });
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
};
const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule(
{ {
@@ -1,4 +1,5 @@
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
import { SupportedPlugin } from '../types/pluginBridges'; import { SupportedPlugin } from '../types/pluginBridges';
@@ -62,45 +63,45 @@ describe('checkEvaluationIntervalGlobalLimit', () => {
describe('getIsIrmPluginPresent', () => { describe('getIsIrmPluginPresent', () => {
it('should return true when IRM plugin is present in config.apps', () => { 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); expect(getIsIrmPluginPresent()).toBe(true);
}); });
it('should return false when IRM plugin is not present in config.apps', () => { it('should return false when IRM plugin is not present in config.apps', () => {
config.apps = { setAppPluginMetas({
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
}; });
expect(getIsIrmPluginPresent()).toBe(false); expect(getIsIrmPluginPresent()).toBe(false);
}); });
}); });
describe('getIrmIfPresentOrIncidentPluginId', () => { describe('getIrmIfPresentOrIncidentPluginId', () => {
it('should return IRM plugin ID when IRM plugin is present', () => { 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); expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm);
}); });
it('should return Incident plugin ID when IRM plugin is not present', () => { it('should return Incident plugin ID when IRM plugin is not present', () => {
config.apps = { setAppPluginMetas({
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
}; });
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident); expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident);
}); });
}); });
describe('getIrmIfPresentOrOnCallPluginId', () => { describe('getIrmIfPresentOrOnCallPluginId', () => {
it('should return IRM plugin ID when IRM plugin is present', () => { 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); expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm);
}); });
it('should return OnCall plugin ID when IRM plugin is not present', () => { it('should return OnCall plugin ID when IRM plugin is not present', () => {
config.apps = { setAppPluginMetas({
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
}; });
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall); expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall);
}); });
}); });
@@ -1,5 +1,5 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config, getAppPluginMetas } from '@grafana/runtime';
import { SupportedPlugin } from '../types/pluginBridges'; import { SupportedPlugin } from '../types/pluginBridges';
@@ -30,7 +30,7 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
} }
export function getIsIrmPluginPresent() { export function getIsIrmPluginPresent() {
return SupportedPlugin.Irm in config.apps; return SupportedPlugin.Irm in getAppPluginMetas();
} }
export function getIrmIfPresentOrIncidentPluginId() { export function getIrmIfPresentOrIncidentPluginId() {
@@ -1,5 +1,5 @@
import { PluginLoadingStrategy } from '@grafana/data'; import { PluginLoadingStrategy } from '@grafana/data';
import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal';
import { RuleGroupIdentifier } from 'app/types/unified-alerting'; import { RuleGroupIdentifier } from 'app/types/unified-alerting';
import { import {
@@ -42,7 +42,7 @@ describe('getRuleOrigin', () => {
}); });
it('returns pluginId when origin label matches expected format and plugin is installed', () => { it('returns pluginId when origin label matches expected format and plugin is installed', () => {
config.apps = { setAppPluginMetas({
installed_plugin: { installed_plugin: {
id: 'installed_plugin', id: 'installed_plugin',
version: '', version: '',
@@ -65,7 +65,7 @@ describe('getRuleOrigin', () => {
}, },
}, },
}, },
}; });
const rule = mockPromAlertingRule({ const rule = mockPromAlertingRule({
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' }, labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' },
}); });
@@ -1,7 +1,7 @@
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
import { AlertState } from '@grafana/data'; import { AlertState } from '@grafana/data';
import { config } from '@grafana/runtime'; import { getAppPluginMeta } from '@grafana/runtime';
import { import {
Alert, Alert,
AlertingRule, AlertingRule,
@@ -273,7 +273,7 @@ export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): R
} }
function isPluginInstalled(pluginId: string) { function isPluginInstalled(pluginId: string) {
return Boolean(config.apps[pluginId]); return Boolean(getAppPluginMeta(pluginId));
} }
export function isPluginProvidedGroup(group: RulerRuleGroupDTO): boolean { export function isPluginProvidedGroup(group: RulerRuleGroupDTO): boolean {
@@ -3,12 +3,14 @@ import userEvent from '@testing-library/user-event';
import { PluginLoadingStrategy } from '@grafana/data'; import { PluginLoadingStrategy } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AdvisorRedirectNotice } from './AdvisorRedirectNotice'; import { AdvisorRedirectNotice } from './AdvisorRedirectNotice';
const originalFeatureToggleValue = config.featureToggles.grafanaAdvisor; const originalFeatureToggleValue = config.featureToggles.grafanaAdvisor;
jest.mock('@grafana/runtime/internal', () => ({ jest.mock('@grafana/runtime/internal', () => ({
...jest.requireActual('@grafana/runtime/internal'),
UserStorage: jest.fn().mockImplementation(() => ({ UserStorage: jest.fn().mockImplementation(() => ({
getItem: jest.fn().mockResolvedValue('true'), getItem: jest.fn().mockResolvedValue('true'),
setItem: jest.fn().mockResolvedValue(undefined), setItem: jest.fn().mockResolvedValue(undefined),
@@ -24,27 +26,29 @@ describe('AdvisorRedirectNotice', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
config.featureToggles.grafanaAdvisor = originalFeatureToggleValue; config.featureToggles.grafanaAdvisor = originalFeatureToggleValue;
config.apps['grafana-advisor-app'] = { setAppPluginMetas({
id: 'grafana-advisor-app', 'grafana-advisor-app': {
path: '/a/grafana-advisor-app', id: 'grafana-advisor-app',
version: '1.0.0', path: '/a/grafana-advisor-app',
preload: false, version: '1.0.0',
angular: { detected: false, hideDeprecation: false }, preload: false,
loadingStrategy: PluginLoadingStrategy.fetch, angular: { detected: false, hideDeprecation: false },
dependencies: { loadingStrategy: PluginLoadingStrategy.fetch,
grafanaDependency: '*', dependencies: {
grafanaVersion: '*', grafanaDependency: '*',
plugins: [], grafanaVersion: '*',
extensions: { exposedComponents: [] }, 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 () => { 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 () => { it('should not render when app is not installed', async () => {
delete config.apps['grafana-advisor-app']; setAppPluginMetas({});
render(<AdvisorRedirectNotice />); render(<AdvisorRedirectNotice />);
expect(screen.queryByRole('status')).not.toBeInTheDocument(); expect(screen.queryByRole('status')).not.toBeInTheDocument();
}); });
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime'; import { config, getAppPluginMeta } from '@grafana/runtime';
import { UserStorage } from '@grafana/runtime/internal'; import { UserStorage } from '@grafana/runtime/internal';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
@@ -28,7 +28,8 @@ export function AdvisorRedirectNotice() {
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
const [showNotice, setShowNotice] = useState(false); 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(() => { useEffect(() => {
if (canUseAdvisor) { if (canUseAdvisor) {
@@ -19,12 +19,7 @@ jest.mock('@grafana/llm', () => ({
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
config: { getAppPluginMeta: (id: string) => ({ [id]: {} }),
...jest.requireActual('@grafana/runtime').config,
apps: {
'grafana-llm-app': true,
},
},
})); }));
describe('getDashboardChanges', () => { describe('getDashboardChanges', () => {
@@ -1,7 +1,7 @@
import { pick } from 'lodash'; import { pick } from 'lodash';
import { llm } from '@grafana/llm'; import { llm } from '@grafana/llm';
import { config } from '@grafana/runtime'; import { getAppPluginMeta } from '@grafana/runtime';
import { Panel } from '@grafana/schema'; import { Panel } from '@grafana/schema';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
@@ -70,7 +70,7 @@ let llmHealthCheck: Promise<boolean> | undefined;
* @returns true if the LLM plugin is enabled. * @returns true if the LLM plugin is enabled.
*/ */
export async function isLLMPluginEnabled(): Promise<boolean> { export async function isLLMPluginEnabled(): Promise<boolean> {
if (!config.apps['grafana-llm-app']) { if (!getAppPluginMeta('grafana-llm-app')) {
return false; return false;
} }
@@ -2,7 +2,8 @@ import React from 'react';
import { firstValueFrom, take } from 'rxjs'; import { firstValueFrom, take } from 'rxjs';
import { PluginLoadingStrategy } from '@grafana/data'; 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 { log } from '../logs/log';
import { resetLogMock } from '../logs/testUtils'; import { resetLogMock } from '../logs/testUtils';
@@ -30,7 +31,7 @@ jest.mock('../logs/log', () => {
}); });
describe('AddedComponentsRegistry', () => { describe('AddedComponentsRegistry', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'grafana-basic-app'; const pluginId = 'grafana-basic-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -61,13 +62,11 @@ describe('AddedComponentsRegistry', () => {
beforeEach(() => { beforeEach(() => {
resetLogMock(log); resetLogMock(log);
jest.mocked(isGrafanaDevMode).mockReturnValue(false); jest.mocked(isGrafanaDevMode).mockReturnValue(false);
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return empty registry when no extensions registered', async () => { it('should return empty registry when no extensions registered', async () => {
@@ -450,7 +449,9 @@ describe('AddedComponentsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -499,7 +500,9 @@ describe('AddedComponentsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -525,7 +528,9 @@ describe('AddedComponentsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -1,7 +1,8 @@
import { firstValueFrom, take } from 'rxjs'; import { firstValueFrom, take } from 'rxjs';
import { PluginLoadingStrategy } from '@grafana/data'; 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 { log } from '../logs/log';
import { resetLogMock } from '../logs/testUtils'; import { resetLogMock } from '../logs/testUtils';
@@ -29,7 +30,7 @@ jest.mock('../logs/log', () => {
}); });
describe('addedFunctionsRegistry', () => { describe('addedFunctionsRegistry', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'grafana-basic-app'; const pluginId = 'grafana-basic-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -60,13 +61,11 @@ describe('addedFunctionsRegistry', () => {
beforeEach(() => { beforeEach(() => {
resetLogMock(log); resetLogMock(log);
jest.mocked(isGrafanaDevMode).mockReturnValue(false); jest.mocked(isGrafanaDevMode).mockReturnValue(false);
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return empty registry when no extensions registered', async () => { it('should return empty registry when no extensions registered', async () => {
@@ -642,7 +641,9 @@ describe('addedFunctionsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -691,7 +692,9 @@ describe('addedFunctionsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -717,7 +720,9 @@ describe('addedFunctionsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -1,7 +1,8 @@
import { firstValueFrom, take } from 'rxjs'; import { firstValueFrom, take } from 'rxjs';
import { PluginLoadingStrategy } from '@grafana/data'; 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 { log } from '../logs/log';
import { resetLogMock } from '../logs/testUtils'; import { resetLogMock } from '../logs/testUtils';
@@ -29,7 +30,7 @@ jest.mock('../logs/log', () => {
}); });
describe('AddedLinksRegistry', () => { describe('AddedLinksRegistry', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'grafana-basic-app'; const pluginId = 'grafana-basic-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -60,13 +61,11 @@ describe('AddedLinksRegistry', () => {
beforeEach(() => { beforeEach(() => {
resetLogMock(log); resetLogMock(log);
jest.mocked(isGrafanaDevMode).mockReturnValue(false); jest.mocked(isGrafanaDevMode).mockReturnValue(false);
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return empty registry when no extensions registered', async () => { it('should return empty registry when no extensions registered', async () => {
@@ -624,7 +623,9 @@ describe('AddedLinksRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -675,7 +676,9 @@ describe('AddedLinksRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -702,7 +705,9 @@ describe('AddedLinksRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -2,7 +2,8 @@ import React from 'react';
import { firstValueFrom, take } from 'rxjs'; import { firstValueFrom, take } from 'rxjs';
import { PluginLoadingStrategy } from '@grafana/data'; 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 { log } from '../logs/log';
import { resetLogMock } from '../logs/testUtils'; import { resetLogMock } from '../logs/testUtils';
@@ -30,7 +31,7 @@ jest.mock('../logs/log', () => {
}); });
describe('ExposedComponentsRegistry', () => { describe('ExposedComponentsRegistry', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'grafana-basic-app'; const pluginId = 'grafana-basic-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -61,13 +62,11 @@ describe('ExposedComponentsRegistry', () => {
beforeEach(() => { beforeEach(() => {
resetLogMock(log); resetLogMock(log);
jest.mocked(isGrafanaDevMode).mockReturnValue(false); jest.mocked(isGrafanaDevMode).mockReturnValue(false);
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return empty registry when no exposed components have been registered', async () => { 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 // 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({ registry.register({
pluginId, pluginId,
@@ -472,7 +473,9 @@ describe('ExposedComponentsRegistry', () => {
}; };
// Make sure that the meta-info is empty // 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({ registry.register({
pluginId, pluginId,
@@ -497,8 +500,9 @@ describe('ExposedComponentsRegistry', () => {
component: () => React.createElement('div', null, 'Hello World1'), component: () => React.createElement('div', null, 'Hello World1'),
}; };
// Make sure that the meta-info is empty const plugin = getAppPluginMeta(pluginId);
config.apps[pluginId].extensions.exposedComponents = [componentConfig]; const config = { ...plugin, extensions: { ...plugin.extensions, exposedComponents: [componentConfig] } };
setAppPluginMetas({ [pluginId]: config });
registry.register({ registry.register({
pluginId, pluginId,
@@ -2,7 +2,8 @@ import { act, render, renderHook, screen, waitFor } from '@testing-library/react
import type { JSX } from 'react'; import type { JSX } from 'react';
import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; 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 { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import { log } from './logs/log'; import { log } from './logs/log';
@@ -51,7 +52,7 @@ describe('usePluginComponent()', () => {
let registries: PluginExtensionRegistries; let registries: PluginExtensionRegistries;
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
let pluginMeta: PluginMeta; let pluginMeta: PluginMeta;
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'myorg-extensions-app'; const pluginId = 'myorg-extensions-app';
const exposedComponentId = `${pluginId}/exposed-component/v1`; const exposedComponentId = `${pluginId}/exposed-component/v1`;
const exposedComponentConfig = { const exposedComponentConfig = {
@@ -135,9 +136,7 @@ describe('usePluginComponent()', () => {
}, },
}; };
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider> <ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
@@ -145,7 +144,7 @@ describe('usePluginComponent()', () => {
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return null if there are no component exposed for the id', () => { it('should return null if there are no component exposed for the id', () => {
@@ -8,7 +8,8 @@ import {
PluginMeta, PluginMeta,
PluginType, PluginType,
} from '@grafana/data'; } 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 { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import * as errors from './errors'; import * as errors from './errors';
@@ -115,31 +116,33 @@ describe('usePluginComponents()', () => {
}, },
}; };
config.apps[pluginId] = { setAppPluginMetas({
id: pluginId, [pluginId]: {
path: '', id: pluginId,
version: '', path: '',
preload: false, version: '',
angular: { preload: false,
detected: false, angular: {
hideDeprecation: false, detected: false,
}, hideDeprecation: false,
loadingStrategy: PluginLoadingStrategy.fetch, },
dependencies: { loadingStrategy: PluginLoadingStrategy.fetch,
grafanaVersion: '8.0.0', dependencies: {
plugins: [], grafanaVersion: '8.0.0',
plugins: [],
extensions: {
exposedComponents: [],
},
},
extensions: { extensions: {
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [], exposedComponents: [],
extensionPoints: [],
}, },
}, },
extensions: { });
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [],
extensionPoints: [],
},
};
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider meta={pluginMeta}> <PluginContextProvider meta={pluginMeta}>
@@ -506,8 +509,10 @@ describe('usePluginComponents()', () => {
component: () => <div>Component</div>, component: () => <div>Component</div>,
}; };
// The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps). // The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata.
config.apps[pluginId].extensions.addedComponents = [componentConfig]; const plugin = getAppPluginMeta(pluginId);
const config = { ...plugin, extensions: { ...plugin.extensions, addedComponents: [componentConfig] } };
setAppPluginMetas({ [pluginId]: config });
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider <PluginContextProvider
@@ -8,7 +8,8 @@ import {
PluginMeta, PluginMeta,
PluginType, PluginType,
} from '@grafana/data'; } from '@grafana/data';
import { config } from '@grafana/runtime'; import { getAppPluginMeta } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import * as errors from './errors'; import * as errors from './errors';
@@ -107,31 +108,33 @@ describe('usePluginFunctions()', () => {
}, },
}; };
config.apps[pluginId] = { setAppPluginMetas({
id: pluginId, [pluginId]: {
path: '', id: pluginId,
version: '', path: '',
preload: false, version: '',
angular: { preload: false,
detected: false, angular: {
hideDeprecation: false, detected: false,
}, hideDeprecation: false,
loadingStrategy: PluginLoadingStrategy.fetch, },
dependencies: { loadingStrategy: PluginLoadingStrategy.fetch,
grafanaVersion: '8.0.0', dependencies: {
plugins: [], grafanaVersion: '8.0.0',
plugins: [],
extensions: {
exposedComponents: [],
},
},
extensions: { extensions: {
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [], exposedComponents: [],
extensionPoints: [],
}, },
}, },
extensions: { });
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [],
extensionPoints: [],
},
};
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider meta={pluginMeta}> <PluginContextProvider meta={pluginMeta}>
@@ -328,8 +331,10 @@ describe('usePluginFunctions()', () => {
fn: () => 'function1', fn: () => 'function1',
}; };
// The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata (config.apps). // The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata.
config.apps[pluginId].extensions.addedFunctions = [functionConfig]; const plugin = getAppPluginMeta(pluginId);
const config = { ...plugin, extensions: { ...plugin.extensions, addedFunctions: [functionConfig] } };
setAppPluginMetas({ [pluginId]: config });
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider <PluginContextProvider
@@ -8,7 +8,8 @@ import {
PluginMeta, PluginMeta,
PluginType, PluginType,
} from '@grafana/data'; } from '@grafana/data';
import { config } from '@grafana/runtime'; import { getAppPluginMeta } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import * as errors from './errors'; import * as errors from './errors';
@@ -107,31 +108,33 @@ describe('usePluginLinks()', () => {
}, },
}; };
config.apps[pluginId] = { setAppPluginMetas({
id: pluginId, [pluginId]: {
path: '', id: pluginId,
version: '', path: '',
preload: false, version: '',
angular: { preload: false,
detected: false, angular: {
hideDeprecation: false, detected: false,
}, hideDeprecation: false,
loadingStrategy: PluginLoadingStrategy.fetch, },
dependencies: { loadingStrategy: PluginLoadingStrategy.fetch,
grafanaVersion: '8.0.0', dependencies: {
plugins: [], grafanaVersion: '8.0.0',
plugins: [],
extensions: {
exposedComponents: [],
},
},
extensions: { extensions: {
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [], exposedComponents: [],
extensionPoints: [],
}, },
}, },
extensions: { });
addedLinks: [],
addedComponents: [],
addedFunctions: [],
exposedComponents: [],
extensionPoints: [],
},
};
wrapper = ({ children }: { children: React.ReactNode }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider meta={pluginMeta}> <PluginContextProvider meta={pluginMeta}>
@@ -266,7 +269,9 @@ describe('usePluginLinks()', () => {
}; };
// The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps). // 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 }) => ( wrapper = ({ children }: { children: React.ReactNode }) => (
<PluginContextProvider <PluginContextProvider
@@ -2,7 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react';
import { type Unsubscribable } from 'rxjs'; import { type Unsubscribable } from 'rxjs';
import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data'; import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data';
import { config, AppPluginConfig } from '@grafana/runtime'; import { config, AppPluginConfig, getAppPluginMetas, getAppPluginMeta } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { appEvents } from 'app/core/app_events'; import { appEvents } from 'app/core/app_events';
import { ShowModalReactEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
@@ -997,7 +998,7 @@ describe('Plugin Extensions / Utils', () => {
}); });
describe('getAppPluginConfigs()', () => { describe('getAppPluginConfigs()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const genereicAppPluginConfig = { const genereicAppPluginConfig = {
path: '', path: '',
version: '', version: '',
@@ -1024,11 +1025,11 @@ describe('Plugin Extensions / Utils', () => {
}; };
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
test('should return the app plugin configs based on the provided plugin ids', () => { test('should return the app plugin configs based on the provided plugin ids', () => {
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1041,16 +1042,16 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-third-app', id: 'myorg-third-app',
}, },
}; });
expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([ expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([
config.apps['myorg-first-app'], getAppPluginMeta('myorg-first-app'),
config.apps['myorg-third-app'], getAppPluginMeta('myorg-third-app'),
]); ]);
}); });
test('should simply ignore the app plugin ids that do not belong to a config', () => { test('should simply ignore the app plugin ids that do not belong to a config', () => {
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1063,9 +1064,9 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-third-app', 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()', () => { describe('getExtensionPointPluginDependencies()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const genereicAppPluginConfig = { const genereicAppPluginConfig = {
path: '', path: '',
version: '', version: '',
@@ -1103,13 +1104,13 @@ describe('Plugin Extensions / Utils', () => {
}; };
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
test('should return the app plugin ids that register extensions to a link extension point', () => { test('should return the app plugin ids that register extensions to a link extension point', () => {
const extensionPointId = 'myorg-first-app/link/v1'; const extensionPointId = 'myorg-first-app/link/v1';
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1135,7 +1136,7 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-third-app', id: 'myorg-third-app',
}, },
}; });
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); 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', () => { test('should return the app plugin ids that register extensions to a component extension point', () => {
const extensionPointId = 'myorg-first-app/component/v1'; const extensionPointId = 'myorg-first-app/component/v1';
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1171,7 +1172,7 @@ describe('Plugin Extensions / Utils', () => {
addedFunctions: [], addedFunctions: [],
}, },
}, },
}; });
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
@@ -1182,7 +1183,7 @@ describe('Plugin Extensions / Utils', () => {
const extensionPointId = 'myorg-first-app/component/v1'; const extensionPointId = 'myorg-first-app/component/v1';
// None of the apps are extending the extension point // None of the apps are extending the extension point
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1195,7 +1196,7 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-third-app', id: 'myorg-third-app',
}, },
}; });
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); 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', () => { 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'; const extensionPointId = 'myorg-first-app/component/v1';
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1281,7 +1282,7 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-sixth-app', id: 'myorg-sixth-app',
}, },
}; });
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId); const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
@@ -1290,7 +1291,7 @@ describe('Plugin Extensions / Utils', () => {
}); });
describe('getExposedComponentPluginDependencies()', () => { describe('getExposedComponentPluginDependencies()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const genereicAppPluginConfig = { const genereicAppPluginConfig = {
path: '', path: '',
version: '', version: '',
@@ -1317,13 +1318,13 @@ describe('Plugin Extensions / Utils', () => {
}; };
afterEach(() => { 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', () => { 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'; const exposedComponentId = 'myorg-second-app/component/v1';
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1348,7 +1349,7 @@ describe('Plugin Extensions / Utils', () => {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-third-app', id: 'myorg-third-app',
}, },
}; });
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId); 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', () => { 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'; const exposedComponentId = 'myorg-second-app/component/v1';
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1427,7 +1428,7 @@ describe('Plugin Extensions / Utils', () => {
addedFunctions: [], addedFunctions: [],
}, },
}, },
}; });
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId); const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId);
@@ -1436,7 +1437,7 @@ describe('Plugin Extensions / Utils', () => {
}); });
describe('getAppPluginDependencies()', () => { describe('getAppPluginDependencies()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const genereicAppPluginConfig = { const genereicAppPluginConfig = {
path: '', path: '',
version: '', version: '',
@@ -1463,11 +1464,11 @@ describe('Plugin Extensions / Utils', () => {
}; };
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
test('should not end up in an infinite loop if there are circular dependencies', () => { test('should not end up in an infinite loop if there are circular dependencies', () => {
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1492,7 +1493,7 @@ describe('Plugin Extensions / Utils', () => {
}, },
}, },
}, },
}; });
const appPluginIds = getAppPluginDependencies('myorg-second-app'); 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', () => { test('should not end up in an infinite loop if a plugin depends on itself', () => {
config.apps = { setAppPluginMetas({
'myorg-first-app': { 'myorg-first-app': {
...genereicAppPluginConfig, ...genereicAppPluginConfig,
id: 'myorg-first-app', id: 'myorg-first-app',
@@ -1517,7 +1518,7 @@ describe('Plugin Extensions / Utils', () => {
}, },
}, },
}, },
}; });
const appPluginIds = getAppPluginDependencies('myorg-second-app'); const appPluginIds = getAppPluginDependencies('myorg-second-app');
@@ -1526,7 +1527,7 @@ describe('Plugin Extensions / Utils', () => {
}); });
describe('getExtensionPointPluginMeta()', () => { describe('getExtensionPointPluginMeta()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const mockExtensionPointId = 'test-extension-point'; const mockExtensionPointId = 'test-extension-point';
const mockApp1: AppPluginConfig = { const mockApp1: AppPluginConfig = {
id: 'app1', id: 'app1',
@@ -1581,28 +1582,28 @@ describe('Plugin Extensions / Utils', () => {
}; };
beforeEach(() => { beforeEach(() => {
config.apps = {}; setAppPluginMetas({});
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return empty map when no plugins have extensions for the point', () => { it('should return empty map when no plugins have extensions for the point', () => {
config.apps = { setAppPluginMetas({
app1: { ...mockApp1, extensions: { ...mockApp1.extensions, addedComponents: [], addedLinks: [] } }, app1: { ...mockApp1, extensions: { ...mockApp1.extensions, addedComponents: [], addedLinks: [] } },
app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } }, app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } },
}; });
const result = getExtensionPointPluginMeta(mockExtensionPointId); const result = getExtensionPointPluginMeta(mockExtensionPointId);
expect(result.size).toBe(0); expect(result.size).toBe(0);
}); });
it('should return map with plugins that have components for the extension point', () => { it('should return map with plugins that have components for the extension point', () => {
config.apps = { setAppPluginMetas({
app1: mockApp1, app1: mockApp1,
app2: mockApp2, app2: mockApp2,
}; });
const result = getExtensionPointPluginMeta(mockExtensionPointId); 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', () => { it('should filter out plugins that do not have any extensions for the point', () => {
config.apps = { setAppPluginMetas({
app1: mockApp1, app1: mockApp1,
app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } }, app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } },
app3: { app3: {
@@ -1630,7 +1631,7 @@ describe('Plugin Extensions / Utils', () => {
addedLinks: [{ title: 'Link 3', targets: ['other-point'] }], addedLinks: [{ title: 'Link 3', targets: ['other-point'] }],
}, },
}, },
}; });
const result = getExtensionPointPluginMeta(mockExtensionPointId); const result = getExtensionPointPluginMeta(mockExtensionPointId);
@@ -16,7 +16,7 @@ import {
PluginExtensionPoints, PluginExtensionPoints,
ExtensionInfo, ExtensionInfo,
} from '@grafana/data'; } 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 { Modal } from '@grafana/ui';
import { appEvents } from 'app/core/app_events'; import { appEvents } from 'app/core/app_events';
import { getPluginSettings } from 'app/features/plugins/pluginSettings'; 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 isGrafanaDevMode = () => config.buildInfo.env === 'development';
export const getAppPluginConfigs = (pluginIds: string[] = []) => 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) => { export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => {
return exposedComponentId.split('/')[0]; 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.) // (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.) // (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.)
export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => { export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => {
return Object.values(config.apps) return Object.values(getAppPluginMetas())
.filter( .filter(
(app) => (app) =>
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) || app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
@@ -646,7 +646,7 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension
return new Map( return new Map(
getExtensionPointPluginDependencies(extensionPointId) getExtensionPointPluginDependencies(extensionPointId)
.map((pluginId) => { .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 the plugin does not exist or does not expose any components or links to the extension point, return undefined
if ( if (
!app || !app ||
@@ -683,7 +683,7 @@ export const getExposedComponentPluginDependencies = (exposedComponentId: string
// metadata field. (For example the plugins that expose components that the app depends on.) // metadata field. (For example the plugins that expose components that the app depends on.)
// Heads up! This is a recursive function. // Heads up! This is a recursive function.
export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => { export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => {
if (!config.apps[pluginId]) { if (!getAppPluginMeta(pluginId)) {
return []; return [];
} }
@@ -692,7 +692,7 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [
return []; return [];
} }
const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map( const pluginIdDependencies = getAppPluginMeta(pluginId).dependencies.extensions.exposedComponents.map(
getAppPluginIdFromExposedComponentId getAppPluginIdFromExposedComponentId
); );
@@ -713,7 +713,7 @@ export const getAppPluginsToAwait = () => {
'cloud-home-app', '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. // 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 awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id);
const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(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)); return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
}); });
}; };
@@ -7,7 +7,8 @@ import {
PluginLoadingStrategy, PluginLoadingStrategy,
PluginType, PluginType,
} from '@grafana/data'; } 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 { createLogMock } from './logs/testUtils';
import { import {
@@ -223,7 +224,7 @@ describe('Plugin Extension Validators', () => {
}); });
describe('isAddedLinkMetaInfoMissing()', () => { describe('isAddedLinkMetaInfoMissing()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'myorg-extensions-app'; const pluginId = 'myorg-extensions-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -257,18 +258,20 @@ describe('Plugin Extension Validators', () => {
}; };
beforeEach(() => { beforeEach(() => {
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return FALSE if the meta-info in the plugin.json is correct', () => { it('should return FALSE if the meta-info in the plugin.json is correct', () => {
const log = createLogMock(); 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); 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', () => { it('should return TRUE and log an error if the app config is not found', () => {
const log = createLogMock(); const log = createLogMock();
delete config.apps[pluginId]; setAppPluginMetas({});
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); 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', () => { it('should return TRUE and log an error if the link has no meta-info in the plugin.json', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedLinks = []; const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedLinks: [] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); 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', () => { it('should return TRUE and log an error if the "targets" do not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedLinks.push(extensionConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedLinkMetaInfoMissing( const returnValue = isAddedLinkMetaInfoMissing(
pluginId, pluginId,
@@ -322,7 +333,11 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE and log a warning if the "description" does not match', () => { it('should return FALSE and log a warning if the "description" does not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedLinks.push(extensionConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedLinks: [extensionConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedLinkMetaInfoMissing( const returnValue = isAddedLinkMetaInfoMissing(
pluginId, pluginId,
@@ -340,12 +355,15 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE with links with the same title but different targets', () => { it('should return FALSE with links with the same title but different targets', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
const extensionConfig2 = { const extensionConfig2 = {
...extensionConfig, ...extensionConfig,
targets: [PluginExtensionPoints.ExploreToolbarAction], 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); const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig2, log);
@@ -355,7 +373,7 @@ describe('Plugin Extension Validators', () => {
}); });
describe('isAddedComponentMetaInfoMissing()', () => { describe('isAddedComponentMetaInfoMissing()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'myorg-extensions-app'; const pluginId = 'myorg-extensions-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -390,18 +408,20 @@ describe('Plugin Extension Validators', () => {
}; };
beforeEach(() => { beforeEach(() => {
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return FALSE if the meta-info in the plugin.json is correct', () => { it('should return FALSE if the meta-info in the plugin.json is correct', () => {
const log = createLogMock(); 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); 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', () => { it('should return TRUE and log an error if the app config is not found', () => {
const log = createLogMock(); const log = createLogMock();
delete config.apps[pluginId]; setAppPluginMetas({});
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); 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', () => { it('should return TRUE and log an error if the Component has no meta-info in the plugin.json', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedComponents = []; const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedComponents: [] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); 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', () => { it('should return TRUE and log an error if the "targets" do not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedComponents.push(extensionConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedComponentMetaInfoMissing( const returnValue = isAddedComponentMetaInfoMissing(
pluginId, pluginId,
@@ -454,7 +482,11 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE and log a warning if the "description" does not match', () => { it('should return FALSE and log a warning if the "description" does not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedComponents.push(extensionConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, addedComponents: [extensionConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isAddedComponentMetaInfoMissing( const returnValue = isAddedComponentMetaInfoMissing(
pluginId, pluginId,
@@ -472,12 +504,15 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE with components with the same title but different targets', () => { it('should return FALSE with components with the same title but different targets', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
const extensionConfig2 = { const extensionConfig2 = {
...extensionConfig, ...extensionConfig,
targets: [PluginExtensionPoints.ExploreToolbarAction], 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); const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig2, log);
@@ -487,7 +522,7 @@ describe('Plugin Extension Validators', () => {
}); });
describe('isExposedComponentMetaInfoMissing()', () => { describe('isExposedComponentMetaInfoMissing()', () => {
const originalApps = config.apps; const originalApps = getAppPluginMetas();
const pluginId = 'myorg-extensions-app'; const pluginId = 'myorg-extensions-app';
const appPluginConfig = { const appPluginConfig = {
id: pluginId, id: pluginId,
@@ -522,18 +557,20 @@ describe('Plugin Extension Validators', () => {
}; };
beforeEach(() => { beforeEach(() => {
config.apps = { setAppPluginMetas({ [pluginId]: appPluginConfig });
[pluginId]: appPluginConfig,
};
}); });
afterEach(() => { afterEach(() => {
config.apps = originalApps; setAppPluginMetas(originalApps);
}); });
it('should return FALSE if the meta-info in the plugin.json is correct', () => { it('should return FALSE if the meta-info in the plugin.json is correct', () => {
const log = createLogMock(); 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); 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', () => { it('should return TRUE and log an error if the app config is not found', () => {
const log = createLogMock(); const log = createLogMock();
delete config.apps[pluginId]; setAppPluginMetas({});
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); 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', () => { it('should return TRUE and log an error if the exposed component has no meta-info in the plugin.json', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.exposedComponents = []; const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, exposedComponents: [] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); 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', () => { it('should return TRUE and log an error if the title does not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, exposedComponents: [exposedComponentConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isExposedComponentMetaInfoMissing( const returnValue = isExposedComponentMetaInfoMissing(
pluginId, pluginId,
@@ -587,7 +632,11 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE and log a warning if the "description" does not match', () => { it('should return FALSE and log a warning if the "description" does not match', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); const config = {
...appPluginConfig,
extensions: { ...appPluginConfig.extensions, exposedComponents: [exposedComponentConfig] },
};
setAppPluginMetas({ [pluginId]: config });
const returnValue = isExposedComponentMetaInfoMissing( const returnValue = isExposedComponentMetaInfoMissing(
pluginId, pluginId,
@@ -605,12 +654,18 @@ describe('Plugin Extension Validators', () => {
it('should return FALSE with components with the same title but different targets', () => { it('should return FALSE with components with the same title but different targets', () => {
const log = createLogMock(); const log = createLogMock();
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
const exposedComponentConfig2 = { const exposedComponentConfig2 = {
...exposedComponentConfig, ...exposedComponentConfig,
targets: [PluginExtensionPoints.ExploreToolbarAction], 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); const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig2, log);
@@ -10,7 +10,7 @@ import {
PluginExtensionPointPatterns, PluginExtensionPointPatterns,
} from '@grafana/data'; } from '@grafana/data';
import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal'; import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal';
import { config, isPluginExtensionLink } from '@grafana/runtime'; import { getAppPluginMeta, isPluginExtensionLink } from '@grafana/runtime';
import * as errors from './errors'; import * as errors from './errors';
import { ExtensionsLog } from './logs/log'; import { ExtensionsLog } from './logs/log';
@@ -151,7 +151,7 @@ export const isAddedLinkMetaInfoMissing = (
log: ExtensionsLog log: ExtensionsLog
) => { ) => {
const logPrefix = 'Could not register link extension. Reason:'; 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; const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null;
if (!app) { if (!app) {
@@ -183,7 +183,7 @@ export const isAddedFunctionMetaInfoMissing = (
log: ExtensionsLog log: ExtensionsLog
) => { ) => {
const logPrefix = 'Could not register function extension. Reason:'; 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; const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null;
if (!app) { if (!app) {
@@ -215,7 +215,7 @@ export const isAddedComponentMetaInfoMissing = (
log: ExtensionsLog log: ExtensionsLog
) => { ) => {
const logPrefix = 'Could not register component extension. Reason:'; const logPrefix = 'Could not register component extension. Reason:';
const app = config.apps[pluginId]; const app = getAppPluginMeta(pluginId);
const pluginJsonMetaInfo = app const pluginJsonMetaInfo = app
? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title) ? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title)
: null; : null;
@@ -249,7 +249,7 @@ export const isExposedComponentMetaInfoMissing = (
log: ExtensionsLog log: ExtensionsLog
) => { ) => {
const logPrefix = 'Could not register exposed component extension. Reason:'; 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; const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null;
if (!app) { if (!app) {
@@ -1,5 +1,5 @@
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data'; import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config, getAppPluginMetas } from '@grafana/runtime';
import { transformPluginSourceForCDN } from '../cdn/utils'; import { transformPluginSourceForCDN } from '../cdn/utils';
import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache'; import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache';
@@ -138,7 +138,7 @@ export function getPluginLoadData(pluginId: string): SandboxPluginMeta {
//find it in apps //find it in apps
//the information inside the apps object is more limited //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) { if (app.id === pluginId) {
return { return {
id: pluginId, id: pluginId,