Compare commits
12 Commits
sriram/SQL
...
hugoh/deco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6024fbb363 | ||
|
|
512f4bc8dc | ||
|
|
88924ee9ac | ||
|
|
5e3c7ad0c1 | ||
|
|
75e08a20f6 | ||
|
|
c8908c5100 | ||
|
|
d8106adb63 | ||
|
|
a4c1b51182 | ||
|
|
535c9be2f7 | ||
|
|
49f891a24d | ||
|
|
86018141d0 | ||
|
|
7fd2476a12 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -653,6 +653,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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export class GrafanaBootConfig {
|
|||||||
snapshotEnabled = true;
|
snapshotEnabled = true;
|
||||||
datasources: { [str: string]: DataSourceInstanceSettings } = {};
|
datasources: { [str: string]: DataSourceInstanceSettings } = {};
|
||||||
panels: { [key: string]: PanelPluginMeta } = {};
|
panels: { [key: string]: PanelPluginMeta } = {};
|
||||||
|
/** @deprecated it will be removed in a future release, use getAppPluginMetas function or useAppPluginMetas hook instead */
|
||||||
apps: Record<string, AppPluginConfigGrafanaData> = {};
|
apps: Record<string, AppPluginConfigGrafanaData> = {};
|
||||||
auth: AuthSettings = {};
|
auth: AuthSettings = {};
|
||||||
minRefreshInterval = '';
|
minRefreshInterval = '';
|
||||||
|
|||||||
@@ -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 { setAppPluginMetas } from '../services/plugins';
|
||||||
|
|||||||
90
packages/grafana-runtime/src/services/plugins.tsx
Normal file
90
packages/grafana-runtime/src/services/plugins.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { AppPluginConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
|
export type AppPluginMetas = Record<string, AppPluginConfig>;
|
||||||
|
|
||||||
|
let apps: AppPluginMetas = {};
|
||||||
|
let appsPromise: Promise<void> | undefined = undefined;
|
||||||
|
|
||||||
|
function areAppsInitialized(): boolean {
|
||||||
|
return Boolean(Object.keys(apps).length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initPluginMetas(): Promise<void> {
|
||||||
|
if (appsPromise) {
|
||||||
|
return appsPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
appsPromise = new Promise((resolve) => {
|
||||||
|
if (config.featureToggles.useMTPlugins) {
|
||||||
|
// add loading app configs from MT API here
|
||||||
|
apps = {};
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
apps = config.apps;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
return appsPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppPluginMetas(): Promise<AppPluginConfig[]> {
|
||||||
|
if (!areAppsInitialized()) {
|
||||||
|
await initPluginMetas();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(cloneDeep(apps));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppPluginMeta(id: string): Promise<AppPluginConfig | undefined> {
|
||||||
|
if (!areAppsInitialized()) {
|
||||||
|
await initPluginMetas();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apps[id]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneDeep(apps[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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseAppPluginMetasResult {
|
||||||
|
isAppPluginMetasLoading: boolean;
|
||||||
|
error: Error | undefined;
|
||||||
|
apps: AppPluginConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppPluginMetas(filterByIds: string[] = []): UseAppPluginMetasResult {
|
||||||
|
const { loading, error, value: apps = [] } = useAsync(getAppPluginMetas);
|
||||||
|
const filtered = apps.filter((app) => filterByIds.includes(app.id));
|
||||||
|
|
||||||
|
return { isAppPluginMetasLoading: loading, error, apps: filtered };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseAppPluginMetaResult {
|
||||||
|
isAppPluginMetaLoading: boolean;
|
||||||
|
error: Error | undefined;
|
||||||
|
app: AppPluginConfig | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppPluginMeta(filterById: string): UseAppPluginMetaResult {
|
||||||
|
const { loading, error, value: app } = useAsync(() => getAppPluginMeta(filterById));
|
||||||
|
return { isAppPluginMetaLoading: loading, error, app };
|
||||||
|
}
|
||||||
@@ -11,3 +11,13 @@
|
|||||||
|
|
||||||
// This is a dummy export so typescript doesn't error importing an "empty module"
|
// This is a dummy export so typescript doesn't error importing an "empty module"
|
||||||
export const unstable = {};
|
export const unstable = {};
|
||||||
|
|
||||||
|
export {
|
||||||
|
type AppPluginMetas,
|
||||||
|
type UseAppPluginMetaResult,
|
||||||
|
type UseAppPluginMetasResult,
|
||||||
|
getAppPluginMeta,
|
||||||
|
getAppPluginMetas,
|
||||||
|
useAppPluginMeta,
|
||||||
|
useAppPluginMetas,
|
||||||
|
} from './services/plugins';
|
||||||
|
|||||||
@@ -99,10 +99,9 @@ import { usePluginComponent } from './features/plugins/extensions/usePluginCompo
|
|||||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
|
||||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||||
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
|
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
|
||||||
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
import { preloadPluginsToBeAwaited, preloadPluginsToBePreloaded } from './features/plugins/pluginPreloader';
|
||||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||||
import { runRequest } from './features/query/state/runRequest';
|
import { runRequest } from './features/query/state/runRequest';
|
||||||
import { initWindowRuntime } from './features/runtime/init';
|
import { initWindowRuntime } from './features/runtime/init';
|
||||||
@@ -257,11 +256,8 @@ export class GrafanaApp {
|
|||||||
const skipAppPluginsPreload =
|
const skipAppPluginsPreload =
|
||||||
config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
|
config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
|
||||||
if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
|
if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
|
||||||
const appPluginsToAwait = getAppPluginsToAwait();
|
preloadPluginsToBePreloaded();
|
||||||
const appPluginsToPreload = getAppPluginsToPreload();
|
await preloadPluginsToBeAwaited();
|
||||||
|
|
||||||
preloadPlugins(appPluginsToPreload);
|
|
||||||
await preloadPlugins(appPluginsToAwait);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHelpNavItemHook(useHelpNode);
|
setHelpNavItemHook(useHelpNode);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useLocalStorage } from 'react-use';
|
|||||||
|
|
||||||
import { PluginExtensionPoints, store } from '@grafana/data';
|
import { PluginExtensionPoints, store } from '@grafana/data';
|
||||||
import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
|
import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
|
||||||
|
import { useAppPluginMetas } from '@grafana/runtime/unstable';
|
||||||
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||||
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
|
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
|
||||||
|
|
||||||
@@ -90,19 +91,21 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
|||||||
// that means, a plugin would need to register both, a link and a component to
|
// that means, a plugin would need to register both, a link and a component to
|
||||||
// `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control
|
// `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control
|
||||||
// whether the component is rendered or not
|
// whether the component is rendered or not
|
||||||
const { links, isLoading } = usePluginLinks({
|
const { links, isLoading: isPluginLinksLoading } = usePluginLinks({
|
||||||
extensionPointId: PluginExtensionPoints.ExtensionSidebar,
|
extensionPointId: PluginExtensionPoints.ExtensionSidebar,
|
||||||
context: {
|
context: {
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { apps, isAppPluginMetasLoading: isAppPluginConfigsLoading } = useAppPluginMetas();
|
||||||
|
const isLoading = isPluginLinksLoading || isAppPluginConfigsLoading;
|
||||||
// get all components for this extension point, but only for the permitted plugins
|
// get all components for this extension point, but only for the permitted plugins
|
||||||
// if the extension sidebar is not enabled, we will return an empty map
|
// if the extension sidebar is not enabled, we will return an empty map
|
||||||
const availableComponents = useMemo(
|
const availableComponents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter(
|
Array.from(getExtensionPointPluginMeta(apps, PluginExtensionPoints.ExtensionSidebar).entries()).filter(
|
||||||
([pluginId, pluginMeta]) =>
|
([pluginId, pluginMeta]) =>
|
||||||
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
|
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
|
||||||
links.some(
|
links.some(
|
||||||
@@ -112,7 +115,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[links]
|
[links, apps]
|
||||||
);
|
);
|
||||||
|
|
||||||
// check if the stored docked component is still available
|
// check if the stored docked component is still available
|
||||||
|
|||||||
@@ -7,6 +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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||||
|
|
||||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||||
@@ -15,6 +16,7 @@ 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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ describe('filterRulerRulesConfig', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should filter by namespace', () => {
|
it('should filter by namespace', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = { [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) };
|
config.apps = { [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) };
|
||||||
const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1');
|
const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1');
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ 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) => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
delete config.apps[pluginId];
|
delete config.apps[pluginId];
|
||||||
server.use(getPluginMissingHandler(pluginId));
|
server.use(getPluginMissingHandler(pluginId));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 }) => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps[id] = {
|
config.apps[id] = {
|
||||||
id,
|
id,
|
||||||
path: baseUrl,
|
path: baseUrl,
|
||||||
|
|||||||
@@ -137,6 +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
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
||||||
};
|
};
|
||||||
@@ -174,6 +175,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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ 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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
config.apps = { [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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||||
@@ -77,11 +79,13 @@ describe('getIsIrmPluginPresent', () => {
|
|||||||
|
|
||||||
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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
config.apps = { [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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||||
@@ -92,11 +96,13 @@ describe('getIrmIfPresentOrIncidentPluginId', () => {
|
|||||||
|
|
||||||
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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
config.apps = { [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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getIsIrmPluginPresent() {
|
export function getIsIrmPluginPresent() {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
return SupportedPlugin.Irm in config.apps;
|
return SupportedPlugin.Irm in config.apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +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', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
config.apps = {
|
config.apps = {
|
||||||
installed_plugin: {
|
installed_plugin: {
|
||||||
id: 'installed_plugin',
|
id: 'installed_plugin',
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): R
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPluginInstalled(pluginId: string) {
|
function isPluginInstalled(pluginId: string) {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
return Boolean(config.apps[pluginId]);
|
return Boolean(config.apps[pluginId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { UserStorage } from '@grafana/runtime/internal';
|
import { UserStorage } from '@grafana/runtime/internal';
|
||||||
|
import { useAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
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';
|
||||||
|
|
||||||
@@ -27,8 +28,9 @@ export function AdvisorRedirectNotice() {
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
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 { app } = useAppPluginMeta('grafana-advisor-app');
|
||||||
|
|
||||||
const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!config.apps['grafana-advisor-app'];
|
const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!app;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canUseAdvisor) {
|
if (canUseAdvisor) {
|
||||||
|
|||||||
@@ -17,14 +17,9 @@ jest.mock('@grafana/llm', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime/unstable', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime/unstable'),
|
||||||
config: {
|
getAppPluginMeta: () => Promise.resolve({}),
|
||||||
...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/unstable';
|
||||||
import { Panel } from '@grafana/schema';
|
import { Panel } from '@grafana/schema';
|
||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
@@ -70,7 +70,8 @@ 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']) {
|
const app = await getAppPluginMeta('grafana-llm-app');
|
||||||
|
if (!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 { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { log } from '../logs/log';
|
import { log } from '../logs/log';
|
||||||
import { resetLogMock } from '../logs/testUtils';
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
@@ -30,7 +31,6 @@ jest.mock('../logs/log', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('AddedComponentsRegistry', () => {
|
describe('AddedComponentsRegistry', () => {
|
||||||
const originalApps = config.apps;
|
|
||||||
const pluginId = 'grafana-basic-app';
|
const pluginId = 'grafana-basic-app';
|
||||||
const appPluginConfig = {
|
const appPluginConfig = {
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
@@ -61,13 +61,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({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no extensions registered', async () => {
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
@@ -450,7 +448,11 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedComponents = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -499,7 +501,11 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedComponents = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -525,7 +531,11 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ export class AddedComponentsRegistry extends Registry<
|
|||||||
super(options);
|
super(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToRegistry(
|
async mapToRegistry(
|
||||||
registry: RegistryType<AddedComponentRegistryItem[]>,
|
registry: RegistryType<AddedComponentRegistryItem[]>,
|
||||||
item: PluginExtensionConfigs<PluginExtensionAddedComponentConfig>
|
item: PluginExtensionConfigs<PluginExtensionAddedComponentConfig>
|
||||||
): RegistryType<AddedComponentRegistryItem[]> {
|
): Promise<RegistryType<AddedComponentRegistryItem[]>> {
|
||||||
const { pluginId, configs } = item;
|
const { pluginId, configs } = item;
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
@@ -51,7 +51,7 @@ export class AddedComponentsRegistry extends Registry<
|
|||||||
if (
|
if (
|
||||||
pluginId !== 'grafana' &&
|
pluginId !== 'grafana' &&
|
||||||
isGrafanaDevMode() &&
|
isGrafanaDevMode() &&
|
||||||
isAddedComponentMetaInfoMissing(pluginId, config, configLog)
|
(await isAddedComponentMetaInfoMissing(pluginId, config, configLog))
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { log } from '../logs/log';
|
import { log } from '../logs/log';
|
||||||
import { resetLogMock } from '../logs/testUtils';
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
@@ -29,7 +30,6 @@ jest.mock('../logs/log', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('addedFunctionsRegistry', () => {
|
describe('addedFunctionsRegistry', () => {
|
||||||
const originalApps = config.apps;
|
|
||||||
const pluginId = 'grafana-basic-app';
|
const pluginId = 'grafana-basic-app';
|
||||||
const appPluginConfig = {
|
const appPluginConfig = {
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
@@ -60,13 +60,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({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no extensions registered', async () => {
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
@@ -642,7 +640,11 @@ describe('addedFunctionsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedFunctions = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -691,7 +693,11 @@ describe('addedFunctionsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedFunctions = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -717,7 +723,11 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [fnConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
|||||||
super(options);
|
super(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToRegistry(
|
async mapToRegistry(
|
||||||
registry: RegistryType<AddedFunctionsRegistryItem[]>,
|
registry: RegistryType<AddedFunctionsRegistryItem[]>,
|
||||||
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
|
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
|
||||||
): RegistryType<AddedFunctionsRegistryItem[]> {
|
): Promise<RegistryType<AddedFunctionsRegistryItem[]>> {
|
||||||
const { pluginId, configs } = item;
|
const { pluginId, configs } = item;
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const configLog = this.logger.child({
|
const configLog = this.logger.child({
|
||||||
title: config.title,
|
title: config.title,
|
||||||
@@ -49,7 +50,11 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
|
if (
|
||||||
|
pluginId !== 'grafana' &&
|
||||||
|
isGrafanaDevMode() &&
|
||||||
|
(await isAddedFunctionMetaInfoMissing(pluginId, config, configLog))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { log } from '../logs/log';
|
import { log } from '../logs/log';
|
||||||
import { resetLogMock } from '../logs/testUtils';
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
@@ -29,7 +30,6 @@ jest.mock('../logs/log', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('AddedLinksRegistry', () => {
|
describe('AddedLinksRegistry', () => {
|
||||||
const originalApps = config.apps;
|
|
||||||
const pluginId = 'grafana-basic-app';
|
const pluginId = 'grafana-basic-app';
|
||||||
const appPluginConfig = {
|
const appPluginConfig = {
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
@@ -60,13 +60,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({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no extensions registered', async () => {
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
@@ -626,7 +624,11 @@ describe('AddedLinksRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedLinks = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -677,7 +679,11 @@ describe('AddedLinksRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.addedLinks = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -704,7 +710,11 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
|||||||
super(options);
|
super(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToRegistry(
|
async mapToRegistry(
|
||||||
registry: RegistryType<AddedLinkRegistryItem[]>,
|
registry: RegistryType<AddedLinkRegistryItem[]>,
|
||||||
item: PluginExtensionConfigs<PluginExtensionAddedLinkConfig>
|
item: PluginExtensionConfigs<PluginExtensionAddedLinkConfig>
|
||||||
): RegistryType<AddedLinkRegistryItem[]> {
|
): Promise<RegistryType<AddedLinkRegistryItem[]>> {
|
||||||
const { pluginId, configs } = item;
|
const { pluginId, configs } = item;
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
@@ -66,7 +66,11 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config, configLog)) {
|
if (
|
||||||
|
pluginId !== 'grafana' &&
|
||||||
|
isGrafanaDevMode() &&
|
||||||
|
(await isAddedLinkMetaInfoMissing(pluginId, config, configLog))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { log } from '../logs/log';
|
import { log } from '../logs/log';
|
||||||
import { resetLogMock } from '../logs/testUtils';
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
@@ -30,7 +31,6 @@ jest.mock('../logs/log', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('ExposedComponentsRegistry', () => {
|
describe('ExposedComponentsRegistry', () => {
|
||||||
const originalApps = config.apps;
|
|
||||||
const pluginId = 'grafana-basic-app';
|
const pluginId = 'grafana-basic-app';
|
||||||
const appPluginConfig = {
|
const appPluginConfig = {
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
@@ -61,13 +61,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({});
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +421,11 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.exposedComponents = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -472,7 +474,11 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure that the meta-info is empty
|
// Make sure that the meta-info is empty
|
||||||
config.apps[pluginId].extensions.exposedComponents = [];
|
const meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -497,8 +503,11 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
config.apps[pluginId].extensions.exposedComponents = [componentConfig];
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [componentConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ export class ExposedComponentsRegistry extends Registry<
|
|||||||
super(options);
|
super(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToRegistry(
|
async mapToRegistry(
|
||||||
registry: RegistryType<ExposedComponentRegistryItem>,
|
registry: RegistryType<ExposedComponentRegistryItem>,
|
||||||
{ pluginId, configs }: PluginExtensionConfigs<PluginExtensionExposedComponentConfig>
|
{ pluginId, configs }: PluginExtensionConfigs<PluginExtensionExposedComponentConfig>
|
||||||
): RegistryType<ExposedComponentRegistryItem> {
|
): Promise<RegistryType<ExposedComponentRegistryItem>> {
|
||||||
if (!configs) {
|
if (!configs) {
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export class ExposedComponentsRegistry extends Registry<
|
|||||||
if (
|
if (
|
||||||
pluginId !== 'grafana' &&
|
pluginId !== 'grafana' &&
|
||||||
isGrafanaDevMode() &&
|
isGrafanaDevMode() &&
|
||||||
isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog)
|
(await isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog))
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Observable, ReplaySubject, Subject, distinctUntilChanged, firstValueFrom, map, scan, startWith } from 'rxjs';
|
import {
|
||||||
|
Observable,
|
||||||
|
ReplaySubject,
|
||||||
|
Subject,
|
||||||
|
distinctUntilChanged,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
mergeScan,
|
||||||
|
startWith,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { ExtensionsLog, log } from '../logs/log';
|
import { ExtensionsLog, log } from '../logs/log';
|
||||||
import { deepFreeze } from '../utils';
|
import { deepFreeze } from '../utils';
|
||||||
@@ -44,7 +53,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
|
|||||||
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
|
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
|
||||||
this.resultSubject
|
this.resultSubject
|
||||||
.pipe(
|
.pipe(
|
||||||
scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
|
mergeScan(this.mapToRegistry.bind(this), options.initialState ?? {}),
|
||||||
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
|
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
|
||||||
startWith(options.initialState ?? {})
|
startWith(options.initialState ?? {})
|
||||||
)
|
)
|
||||||
@@ -55,7 +64,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
|
|||||||
abstract mapToRegistry(
|
abstract mapToRegistry(
|
||||||
registry: RegistryType<TRegistryValue>,
|
registry: RegistryType<TRegistryValue>,
|
||||||
item: PluginExtensionConfigs<TMapType>
|
item: PluginExtensionConfigs<TMapType>
|
||||||
): RegistryType<TRegistryValue>;
|
): Promise<RegistryType<TRegistryValue>>;
|
||||||
|
|
||||||
register(result: PluginExtensionConfigs<TMapType>): void {
|
register(result: PluginExtensionConfigs<TMapType>): void {
|
||||||
if (this.isReadOnly) {
|
if (this.isReadOnly) {
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { preloadPlugins } from '../pluginPreloader';
|
import { PreloadAppPluginsPredicate, preloadPluginsWithPredicate } from '../pluginPreloader';
|
||||||
|
|
||||||
import { getAppPluginConfigs } from './utils';
|
export function useLoadAppPlugins(extensionId: string, predicate: PreloadAppPluginsPredicate): { isLoading: boolean } {
|
||||||
|
const { loading: isLoading } = useAsync(
|
||||||
|
() => preloadPluginsWithPredicate(extensionId, predicate),
|
||||||
|
[extensionId, predicate]
|
||||||
|
);
|
||||||
|
|
||||||
export function useLoadAppPlugins(pluginIds: string[] = []): { isLoading: boolean } {
|
return { isLoading: isLoading };
|
||||||
const { loading: isLoading } = useAsync(async () => {
|
|
||||||
const appConfigs = getAppPluginConfigs(pluginIds);
|
|
||||||
|
|
||||||
if (!appConfigs.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await preloadPlugins(appConfigs);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { isLoading };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 } 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,6 @@ 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 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 +135,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 +143,7 @@ describe('usePluginComponent()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
config.apps = originalApps;
|
setAppPluginMetas({});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { isExposedComponentDependencyMissing } from './validators';
|
|||||||
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
||||||
const registryItem = useExposedComponentRegistrySlice<Props>(id);
|
const registryItem = useExposedComponentRegistrySlice<Props>(id);
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id));
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(id, getExposedComponentPluginDependencies);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
PluginType,
|
PluginType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
@@ -115,31 +117,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}>
|
||||||
@@ -496,7 +500,7 @@ describe('usePluginComponents()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||||
it('should not validate the extension point meta-info for core plugins', () => {
|
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
const componentConfig = {
|
const componentConfig = {
|
||||||
@@ -506,8 +510,12 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<PluginContextProvider
|
<PluginContextProvider
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
}: UsePluginComponentsOptions): UsePluginComponentsResult<Props> {
|
}: UsePluginComponentsOptions): UsePluginComponentsResult<Props> {
|
||||||
const registryItems = useAddedComponentsRegistrySlice<Props>(extensionPointId);
|
const registryItems = useAddedComponentsRegistrySlice<Props>(extensionPointId);
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
PluginMeta,
|
PluginMeta,
|
||||||
PluginType,
|
PluginType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
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}>
|
||||||
@@ -318,7 +321,7 @@ describe('usePluginFunctions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||||
it('should not validate the extension point meta-info for core plugins', () => {
|
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
const functionConfig = {
|
const functionConfig = {
|
||||||
@@ -328,8 +331,12 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [functionConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<PluginContextProvider
|
<PluginContextProvider
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export function usePluginFunctions<Signature>({
|
|||||||
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
|
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
|
||||||
const registryItems = useAddedFunctionsRegistrySlice<Signature>(extensionPointId);
|
const registryItems = useAddedFunctionsRegistrySlice<Signature>(extensionPointId);
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
const deps = getExtensionPointPluginDependencies(extensionPointId);
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
PluginMeta,
|
PluginMeta,
|
||||||
PluginType,
|
PluginType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
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}>
|
||||||
@@ -258,7 +261,7 @@ describe('usePluginLinks()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||||
it('should not validate the extension point meta-info for core plugins', () => {
|
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
const linkConfig = {
|
const linkConfig = {
|
||||||
@@ -269,7 +272,11 @@ 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 meta = await getAppPluginMeta(pluginId);
|
||||||
|
expect(meta).toBeDefined();
|
||||||
|
|
||||||
|
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
|
||||||
|
setAppPluginMetas({ [pluginId]: app });
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<PluginContextProvider
|
<PluginContextProvider
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function usePluginLinks({
|
|||||||
}: UsePluginLinksOptions): UsePluginLinksResult {
|
}: UsePluginLinksOptions): UsePluginLinksResult {
|
||||||
const registryItems = useAddedLinksRegistrySlice(extensionPointId);
|
const registryItems = useAddedLinksRegistrySlice(extensionPointId);
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const { result, pointLog } = validateExtensionPoint({
|
const { result, pointLog } = validateExtensionPoint({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import * as React from 'react';
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type AppPluginConfig,
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
type PluginExtensionOpenModalOptions,
|
type PluginExtensionOpenModalOptions,
|
||||||
isDateTime,
|
isDateTime,
|
||||||
@@ -13,10 +14,9 @@ import {
|
|||||||
PanelMenuItem,
|
PanelMenuItem,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
PluginExtensionPoints,
|
|
||||||
ExtensionInfo,
|
ExtensionInfo,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime';
|
import { reportInteraction, config } 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';
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from 'app/types/events';
|
} from 'app/types/events';
|
||||||
|
|
||||||
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
|
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
|
||||||
|
import { PreloadAppPluginsPredicate } from '../pluginPreloader';
|
||||||
|
|
||||||
import { ExtensionErrorBoundary } from './ExtensionErrorBoundary';
|
import { ExtensionErrorBoundary } from './ExtensionErrorBoundary';
|
||||||
import { ExtensionsLog, log as baseLog } from './logs/log';
|
import { ExtensionsLog, log as baseLog } from './logs/log';
|
||||||
@@ -609,9 +610,6 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string,
|
|||||||
// Can be set with the `GF_DEFAULT_APP_MODE` environment variable
|
// Can be set with the `GF_DEFAULT_APP_MODE` environment variable
|
||||||
export const isGrafanaDevMode = () => config.buildInfo.env === 'development';
|
export const isGrafanaDevMode = () => config.buildInfo.env === 'development';
|
||||||
|
|
||||||
export const getAppPluginConfigs = (pluginIds: string[] = []) =>
|
|
||||||
Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
|
||||||
|
|
||||||
export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => {
|
export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => {
|
||||||
return exposedComponentId.split('/')[0];
|
return exposedComponentId.split('/')[0];
|
||||||
};
|
};
|
||||||
@@ -619,8 +617,11 @@ export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string)
|
|||||||
// Returns a list of app plugin ids that are registering extensions to this extension point.
|
// Returns a list of app plugin ids that are registering extensions to this extension point.
|
||||||
// (These plugins are necessary to be loaded to use the extension point.)
|
// (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: PreloadAppPluginsPredicate = (
|
||||||
return Object.values(config.apps)
|
apps: AppPluginConfig[],
|
||||||
|
extensionPointId: string
|
||||||
|
): string[] => {
|
||||||
|
return apps
|
||||||
.filter(
|
.filter(
|
||||||
(app) =>
|
(app) =>
|
||||||
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
|
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
|
||||||
@@ -628,7 +629,7 @@ export const getExtensionPointPluginDependencies = (extensionPointId: string): s
|
|||||||
)
|
)
|
||||||
.map((app) => app.id)
|
.map((app) => app.id)
|
||||||
.reduce((acc: string[], id: string) => {
|
.reduce((acc: string[], id: string) => {
|
||||||
return [...acc, id, ...getAppPluginDependencies(id)];
|
return [...acc, id, ...getAppPluginDependencies(apps, id)];
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -645,11 +646,14 @@ export type ExtensionPointPluginMeta = Map<
|
|||||||
* @param extensionPointId - The id of the extension point.
|
* @param extensionPointId - The id of the extension point.
|
||||||
* @returns A map of plugin ids and their addedComponents and addedLinks to the extension point.
|
* @returns A map of plugin ids and their addedComponents and addedLinks to the extension point.
|
||||||
*/
|
*/
|
||||||
export const getExtensionPointPluginMeta = (extensionPointId: string): ExtensionPointPluginMeta => {
|
export const getExtensionPointPluginMeta = (
|
||||||
|
apps: AppPluginConfig[],
|
||||||
|
extensionPointId: string
|
||||||
|
): ExtensionPointPluginMeta => {
|
||||||
return new Map(
|
return new Map(
|
||||||
getExtensionPointPluginDependencies(extensionPointId)
|
getExtensionPointPluginDependencies(apps, extensionPointId)
|
||||||
.map((pluginId) => {
|
.map((pluginId) => {
|
||||||
const app = config.apps[pluginId];
|
const app = apps.find((a) => a.id === pluginId);
|
||||||
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
|
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
|
||||||
if (
|
if (
|
||||||
!app ||
|
!app ||
|
||||||
@@ -674,19 +678,27 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension
|
|||||||
|
|
||||||
// Returns a list of app plugin ids that are necessary to be loaded to use the exposed component.
|
// Returns a list of app plugin ids that are necessary to be loaded to use the exposed component.
|
||||||
// (It is first the plugin that exposes the component, and then the ones that it depends on.)
|
// (It is first the plugin that exposes the component, and then the ones that it depends on.)
|
||||||
export const getExposedComponentPluginDependencies = (exposedComponentId: string) => {
|
export const getExposedComponentPluginDependencies: PreloadAppPluginsPredicate = (
|
||||||
|
apps: AppPluginConfig[],
|
||||||
|
exposedComponentId: string
|
||||||
|
) => {
|
||||||
const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId);
|
const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId);
|
||||||
|
|
||||||
return [pluginId].reduce((acc: string[], pluginId: string) => {
|
return [pluginId].reduce((acc: string[], pluginId: string) => {
|
||||||
return [...acc, pluginId, ...getAppPluginDependencies(pluginId)];
|
return [...acc, pluginId, ...getAppPluginDependencies(apps, pluginId)];
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions`
|
// Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions`
|
||||||
// metadata field. (For example the plugins that expose components that the app depends on.)
|
// 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 = (
|
||||||
if (!config.apps[pluginId]) {
|
apps: AppPluginConfig[],
|
||||||
|
pluginId: string,
|
||||||
|
visited: string[] = []
|
||||||
|
): string[] => {
|
||||||
|
const app = apps.find((a) => a.id === pluginId);
|
||||||
|
if (!app) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,38 +707,14 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map(
|
const pluginIdDependencies = app.dependencies.extensions.exposedComponents.map(getAppPluginIdFromExposedComponentId);
|
||||||
getAppPluginIdFromExposedComponentId
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
pluginIdDependencies
|
pluginIdDependencies
|
||||||
.reduce((acc, _pluginId) => {
|
.reduce((acc, _pluginId) => {
|
||||||
return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])];
|
return [...acc, ...getAppPluginDependencies(apps, _pluginId, [...visited, pluginId])];
|
||||||
}, pluginIdDependencies)
|
}, pluginIdDependencies)
|
||||||
// We don't want the plugin to "depend on itself"
|
// We don't want the plugin to "depend on itself"
|
||||||
.filter((id) => id !== pluginId)
|
.filter((id) => id !== pluginId)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a list of app plugins that has to be loaded before core Grafana could finish the initialization.
|
|
||||||
export const getAppPluginsToAwait = () => {
|
|
||||||
const pluginIds = [
|
|
||||||
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
|
|
||||||
'cloud-home-app',
|
|
||||||
];
|
|
||||||
|
|
||||||
return Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization.
|
|
||||||
export const getAppPluginsToPreload = () => {
|
|
||||||
// The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading.
|
|
||||||
const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(PluginExtensionPoints.DashboardPanelMenu);
|
|
||||||
const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id);
|
|
||||||
const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id);
|
|
||||||
|
|
||||||
return Object.values(config.apps).filter((app) => {
|
|
||||||
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@ 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 { isPluginExtensionLink } from '@grafana/runtime';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { ExtensionsLog } from './logs/log';
|
import { ExtensionsLog } from './logs/log';
|
||||||
@@ -145,13 +146,13 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P
|
|||||||
return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id);
|
return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAddedLinkMetaInfoMissing = (
|
export const isAddedLinkMetaInfoMissing = async (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionAddedLinkConfig,
|
metaInfo: PluginExtensionAddedLinkConfig,
|
||||||
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 = await 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) {
|
||||||
@@ -177,13 +178,13 @@ export const isAddedLinkMetaInfoMissing = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAddedFunctionMetaInfoMissing = (
|
export const isAddedFunctionMetaInfoMissing = async (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionAddedFunctionConfig,
|
metaInfo: PluginExtensionAddedFunctionConfig,
|
||||||
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 = await 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) {
|
||||||
@@ -209,13 +210,13 @@ export const isAddedFunctionMetaInfoMissing = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAddedComponentMetaInfoMissing = (
|
export const isAddedComponentMetaInfoMissing = async (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionAddedComponentConfig,
|
metaInfo: PluginExtensionAddedComponentConfig,
|
||||||
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 = await 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;
|
||||||
@@ -243,13 +244,13 @@ export const isAddedComponentMetaInfoMissing = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isExposedComponentMetaInfoMissing = (
|
export const isExposedComponentMetaInfoMissing = async (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionExposedComponentConfig,
|
metaInfo: PluginExtensionExposedComponentConfig,
|
||||||
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 = await 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,12 +1,15 @@
|
|||||||
import type {
|
import {
|
||||||
AppPluginConfig,
|
type AppPluginConfig,
|
||||||
PluginExtensionAddedLinkConfig,
|
type PluginExtensionAddedLinkConfig,
|
||||||
PluginExtensionExposedComponentConfig,
|
type PluginExtensionExposedComponentConfig,
|
||||||
PluginExtensionAddedComponentConfig,
|
type PluginExtensionAddedComponentConfig,
|
||||||
|
PluginExtensionPoints,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { getAppPluginMetas } from '@grafana/runtime/unstable';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||||
|
|
||||||
|
import { getExtensionPointPluginDependencies } from './extensions/utils';
|
||||||
import { pluginImporter } from './importer/pluginImporter';
|
import { pluginImporter } from './importer/pluginImporter';
|
||||||
|
|
||||||
export type PluginPreloadResult = {
|
export type PluginPreloadResult = {
|
||||||
@@ -23,6 +26,59 @@ export const clearPreloadedPluginsCache = () => {
|
|||||||
preloadPromises.clear();
|
preloadPromises.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getAppPluginIdsToAwait() {
|
||||||
|
const pluginIds = [
|
||||||
|
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
|
||||||
|
'cloud-home-app',
|
||||||
|
];
|
||||||
|
|
||||||
|
return pluginIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotAwaited(app: AppPluginConfig) {
|
||||||
|
return !getAppPluginIdsToAwait().includes(app.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadPluginsToBeAwaited() {
|
||||||
|
const apps = await getAppPluginMetas();
|
||||||
|
const awaited = getAppPluginIdsToAwait();
|
||||||
|
const filtered = apps.filter((app) => awaited.includes(app.id));
|
||||||
|
|
||||||
|
preloadPlugins(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadPluginsToBePreloaded() {
|
||||||
|
const apps = await getAppPluginMetas();
|
||||||
|
|
||||||
|
// The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading.
|
||||||
|
const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(
|
||||||
|
apps,
|
||||||
|
PluginExtensionPoints.DashboardPanelMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = apps.filter((app) => {
|
||||||
|
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
preloadPlugins(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreloadAppPluginsPredicate = (apps: AppPluginConfig[], extensionId: string) => string[];
|
||||||
|
|
||||||
|
const noop: PreloadAppPluginsPredicate = () => [];
|
||||||
|
|
||||||
|
export async function preloadPluginsWithPredicate(extensionId: string, predicate: PreloadAppPluginsPredicate = noop) {
|
||||||
|
const apps = await getAppPluginMetas();
|
||||||
|
const filteredIds = predicate(apps, extensionId);
|
||||||
|
const filtered = apps.filter((app) => filteredIds.includes(app.id));
|
||||||
|
|
||||||
|
if (!filtered.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preloadPlugins(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
||||||
// Create preload promises for each app, reusing existing promises if already loading
|
// Create preload promises for each app, reusing existing promises if already loading
|
||||||
const promises = apps.map((app) => {
|
const promises = apps.map((app) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
|
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||||
|
|
||||||
import { transformPluginSourceForCDN } from '../cdn/utils';
|
import { transformPluginSourceForCDN } from '../cdn/utils';
|
||||||
import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache';
|
import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache';
|
||||||
@@ -121,7 +122,7 @@ export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPluginLoadData(pluginId: string): SandboxPluginMeta {
|
export async function getPluginLoadData(pluginId: string): Promise<SandboxPluginMeta> {
|
||||||
// find it in datasources
|
// find it in datasources
|
||||||
for (const datasource of Object.values(config.datasources)) {
|
for (const datasource of Object.values(config.datasources)) {
|
||||||
if (datasource.type === pluginId) {
|
if (datasource.type === pluginId) {
|
||||||
@@ -138,16 +139,15 @@ 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)) {
|
const app = await getAppPluginMeta(pluginId);
|
||||||
if (app.id === pluginId) {
|
if (!app) {
|
||||||
return {
|
throw new Error(`Could not find plugin ${pluginId}`);
|
||||||
id: pluginId,
|
|
||||||
type: PluginType.app,
|
|
||||||
module: app.path,
|
|
||||||
moduleHash: app.moduleHash,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Could not find plugin ${pluginId}`);
|
return {
|
||||||
|
id: pluginId,
|
||||||
|
type: PluginType.app,
|
||||||
|
module: app.path,
|
||||||
|
moduleHash: app.moduleHash,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const pluginLogCache: Record<string, boolean> = {};
|
|||||||
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
|
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
|
||||||
patchWebAPIs();
|
patchWebAPIs();
|
||||||
try {
|
try {
|
||||||
const pluginMeta = getPluginLoadData(pluginId);
|
const pluginMeta = await getPluginLoadData(pluginId);
|
||||||
if (!pluginImportCache.has(pluginId)) {
|
if (!pluginImportCache.has(pluginId)) {
|
||||||
pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta));
|
pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user