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/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
+36
View File
@@ -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',
},
],
},
},
+1
View File
@@ -69,3 +69,4 @@ export {
getCorrelationsService,
setCorrelationsService,
} from './services/CorrelationsService';
export { getAppPluginMetas, getAppPluginMeta, type AppPluginMetas } from './services/plugins';
@@ -29,3 +29,4 @@ export {
export { UserStorage } from '../utils/userStorage';
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';
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);
@@ -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/'
@@ -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({
@@ -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));
};
@@ -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 } }) => {
@@ -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(
{
@@ -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);
});
});
@@ -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() {
@@ -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' },
});
@@ -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 {
@@ -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(<AdvisorRedirectNotice />);
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
@@ -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) {
@@ -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', () => {
@@ -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<boolean> | undefined;
* @returns true if the LLM plugin is enabled.
*/
export async function isLLMPluginEnabled(): Promise<boolean> {
if (!config.apps['grafana-llm-app']) {
if (!getAppPluginMeta('grafana-llm-app')) {
return false;
}
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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 }) => (
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
@@ -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', () => {
@@ -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 }) => (
<PluginContextProvider meta={pluginMeta}>
@@ -506,8 +509,10 @@ describe('usePluginComponents()', () => {
component: () => <div>Component</div>,
};
// 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 }) => (
<PluginContextProvider
@@ -8,7 +8,8 @@ import {
PluginMeta,
PluginType,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { getAppPluginMeta } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import * as errors from './errors';
@@ -107,31 +108,33 @@ describe('usePluginFunctions()', () => {
},
};
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 }) => (
<PluginContextProvider meta={pluginMeta}>
@@ -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 }) => (
<PluginContextProvider
@@ -8,7 +8,8 @@ import {
PluginMeta,
PluginType,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { getAppPluginMeta } from '@grafana/runtime';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import * as errors from './errors';
@@ -107,31 +108,33 @@ describe('usePluginLinks()', () => {
},
};
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 }) => (
<PluginContextProvider meta={pluginMeta}>
@@ -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 }) => (
<PluginContextProvider
@@ -2,7 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react';
import { type Unsubscribable } from 'rxjs';
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 { ShowModalReactEvent } from 'app/types/events';
@@ -997,7 +998,7 @@ describe('Plugin Extensions / Utils', () => {
});
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);
@@ -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));
});
};
@@ -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);
@@ -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) {
@@ -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,