mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin Extensions: Only load app plugins when necessary (#86624)
* feat(plugins): automatically preload plugins This PR enables auto-preloading for plugins when they are used by an extension or extension-point. Once this change is merged plugins that were only using "preload: true" in their plugin.json for using extensions can remove it. * fix: remove unused types * fix: call `setComponentsFromLegacyExports()` after meta is initialised
This commit is contained in:
parent
a2c407854f
commit
cce943b3af
@ -32,4 +32,4 @@ Note that this plugin extends the `@grafana/plugin-configs` configs which is why
|
|||||||
|
|
||||||
## Run Playwright tests
|
## Run Playwright tests
|
||||||
|
|
||||||
- `yarn playwright --project extensions-test-app`
|
- `yarn playwright test --project extensions-test-app`
|
||||||
|
@ -22,6 +22,7 @@ import { RouteDescriptor } from './core/navigation/types';
|
|||||||
import { ThemeProvider } from './core/utils/ConfigProvider';
|
import { ThemeProvider } from './core/utils/ConfigProvider';
|
||||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||||
import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext';
|
||||||
|
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||||
import { ExperimentalSplitPaneRouterWrapper, RouterWrapper } from './routes/RoutesWrapper';
|
import { ExperimentalSplitPaneRouterWrapper, RouterWrapper } from './routes/RoutesWrapper';
|
||||||
|
|
||||||
interface AppWrapperProps {
|
interface AppWrapperProps {
|
||||||
@ -104,7 +105,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
|
|||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
<MaybeTimeRangeProvider>
|
<MaybeTimeRangeProvider>
|
||||||
<SidecarContext_EXPERIMENTAL.Provider value={sidecarServiceSingleton_EXPERIMENTAL}>
|
<SidecarContext_EXPERIMENTAL.Provider value={sidecarServiceSingleton_EXPERIMENTAL}>
|
||||||
<ExtensionRegistriesProvider registries={app.pluginExtensionsRegistries}>
|
<ExtensionRegistriesProvider registries={pluginExtensionRegistries}>
|
||||||
<div className="grafana-app">
|
<div className="grafana-app">
|
||||||
{config.featureToggles.appSidecar ? (
|
{config.featureToggles.appSidecar ? (
|
||||||
<ExperimentalSplitPaneRouterWrapper {...routerWrapperProps} />
|
<ExperimentalSplitPaneRouterWrapper {...routerWrapperProps} />
|
||||||
|
@ -85,12 +85,12 @@ import { PanelDataErrorView } from './features/panel/components/PanelDataErrorVi
|
|||||||
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
||||||
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
||||||
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
||||||
import { setupPluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||||
import { PluginExtensionRegistries } from './features/plugins/extensions/registry/types';
|
|
||||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||||
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 { preloadPlugins } from './features/plugins/pluginPreloader';
|
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
||||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||||
@ -127,7 +127,6 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
|
|
||||||
export class GrafanaApp {
|
export class GrafanaApp {
|
||||||
context!: GrafanaContextType;
|
context!: GrafanaContextType;
|
||||||
pluginExtensionsRegistries!: PluginExtensionRegistries;
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
@ -217,22 +216,16 @@ export class GrafanaApp {
|
|||||||
setDataSourceSrv(dataSourceSrv);
|
setDataSourceSrv(dataSourceSrv);
|
||||||
initWindowRuntime();
|
initWindowRuntime();
|
||||||
|
|
||||||
// Initialize plugin extensions
|
|
||||||
this.pluginExtensionsRegistries = setupPluginExtensionRegistries();
|
|
||||||
|
|
||||||
if (contextSrv.user.orgRole !== '') {
|
if (contextSrv.user.orgRole !== '') {
|
||||||
// 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.
|
const appPluginsToAwait = getAppPluginsToAwait();
|
||||||
// TODO: remove the following exception once the issue mentioned above is fixed.
|
const appPluginsToPreload = getAppPluginsToPreload();
|
||||||
const awaitedAppPluginIds = ['cloud-home-app'];
|
|
||||||
const awaitedAppPlugins = Object.values(config.apps).filter((app) => awaitedAppPluginIds.includes(app.id));
|
|
||||||
const appPlugins = Object.values(config.apps).filter((app) => !awaitedAppPluginIds.includes(app.id));
|
|
||||||
|
|
||||||
preloadPlugins(appPlugins, this.pluginExtensionsRegistries);
|
preloadPlugins(appPluginsToPreload);
|
||||||
await preloadPlugins(awaitedAppPlugins, this.pluginExtensionsRegistries, 'frontend_awaited_plugins_preload');
|
await preloadPlugins(appPluginsToAwait);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPluginExtensionGetter(createPluginExtensionsGetter(this.pluginExtensionsRegistries));
|
setPluginExtensionGetter(createPluginExtensionsGetter(pluginExtensionRegistries));
|
||||||
setPluginExtensionsHook(createUsePluginExtensions(this.pluginExtensionsRegistries));
|
setPluginExtensionsHook(createUsePluginExtensions(pluginExtensionRegistries));
|
||||||
setPluginLinksHook(usePluginLinks);
|
setPluginLinksHook(usePluginLinks);
|
||||||
setPluginComponentHook(usePluginComponent);
|
setPluginComponentHook(usePluginComponent);
|
||||||
setPluginComponentsHook(usePluginComponents);
|
setPluginComponentsHook(usePluginComponents);
|
||||||
|
@ -11,7 +11,9 @@ import { contextSrv } from 'app/core/services/context_srv';
|
|||||||
import { Echo } from 'app/core/services/echo/Echo';
|
import { Echo } from 'app/core/services/echo/Echo';
|
||||||
|
|
||||||
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
||||||
import { setupPluginExtensionRegistries } from '../extensions/registry/setup';
|
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
||||||
|
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
||||||
|
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
import { importAppPlugin } from '../plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
|
|
||||||
@ -86,7 +88,11 @@ function renderUnderRouter(page = '') {
|
|||||||
|
|
||||||
appPluginNavItem.parentItem = appsSection;
|
appPluginNavItem.parentItem = appsSection;
|
||||||
|
|
||||||
const registries = setupPluginExtensionRegistries();
|
const registries = {
|
||||||
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
};
|
||||||
const pagePath = page ? `/${page}` : '';
|
const pagePath = page ? `/${page}` : '';
|
||||||
const route = {
|
const route = {
|
||||||
path: `/a/:pluginId/*`,
|
path: `/a/:pluginId/*`,
|
||||||
|
@ -5,17 +5,17 @@ import { AddedLinksRegistry } from './AddedLinksRegistry';
|
|||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './types';
|
import { PluginExtensionRegistries } from './types';
|
||||||
|
|
||||||
export function setupPluginExtensionRegistries(): PluginExtensionRegistries {
|
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||||
const pluginExtensionsRegistries = {
|
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
export const addedLinksRegistry = new AddedLinksRegistry();
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedComponentsRegistry,
|
||||||
};
|
exposedComponentsRegistry,
|
||||||
|
addedLinksRegistry,
|
||||||
|
};
|
||||||
|
|
||||||
pluginExtensionsRegistries.addedLinksRegistry.register({
|
// Registering core extensions
|
||||||
pluginId: 'grafana',
|
addedLinksRegistry.register({
|
||||||
configs: getCoreExtensionConfigurations(),
|
pluginId: 'grafana',
|
||||||
});
|
configs: getCoreExtensionConfigurations(),
|
||||||
|
});
|
||||||
return pluginExtensionsRegistries;
|
|
||||||
}
|
|
||||||
|
19
public/app/features/plugins/extensions/useLoadAppPlugins.tsx
Normal file
19
public/app/features/plugins/extensions/useLoadAppPlugins.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { preloadPlugins } from '../pluginPreloader';
|
||||||
|
|
||||||
|
import { getAppPluginConfigs } from './utils';
|
||||||
|
|
||||||
|
export function useLoadAppPlugins(pluginIds: string[] = []): { isLoading: boolean } {
|
||||||
|
const { loading: isLoading } = useAsync(async () => {
|
||||||
|
const appConfigs = getAppPluginConfigs(pluginIds);
|
||||||
|
|
||||||
|
if (!appConfigs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await preloadPlugins(appConfigs);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isLoading };
|
||||||
|
}
|
@ -7,11 +7,15 @@ import { config } from '@grafana/runtime';
|
|||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
import { usePluginComponent } from './usePluginComponent';
|
import { usePluginComponent } from './usePluginComponent';
|
||||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
|
|
||||||
|
jest.mock('./useLoadAppPlugins');
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
id: 'my-app-plugin',
|
id: 'my-app-plugin',
|
||||||
@ -83,7 +87,12 @@ describe('usePluginComponent()', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = {
|
||||||
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
};
|
||||||
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
resetLogMock(log);
|
resetLogMock(log);
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@ import { UsePluginComponentResult } from '@grafana/runtime';
|
|||||||
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
|
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
|
import { getExposedComponentPluginDependencies, isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
import { isExposedComponentDependencyMissing } from './validators';
|
import { isExposedComponentDependencyMissing } from './validators';
|
||||||
|
|
||||||
// Returns a component exposed by a plugin.
|
// Returns a component exposed by a plugin.
|
||||||
@ -16,11 +17,19 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl
|
|||||||
const registry = useExposedComponentsRegistry();
|
const registry = useExposedComponentsRegistry();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id));
|
||||||
|
|
||||||
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.
|
||||||
const enableRestrictions = isGrafanaDevMode() && pluginContext;
|
const enableRestrictions = isGrafanaDevMode() && pluginContext;
|
||||||
|
|
||||||
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!registryState?.[id]) {
|
if (!registryState?.[id]) {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -47,5 +56,5 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component, componentLog),
|
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component, componentLog),
|
||||||
};
|
};
|
||||||
}, [id, pluginContext, registryState]);
|
}, [id, pluginContext, registryState, isLoadingAppPlugins]);
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,15 @@ import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
|||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
import { usePluginComponents } from './usePluginComponents';
|
import { usePluginComponents } from './usePluginComponents';
|
||||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
|
|
||||||
|
jest.mock('./useLoadAppPlugins');
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
id: 'my-app-plugin',
|
id: 'my-app-plugin',
|
||||||
@ -50,8 +54,14 @@ describe('usePluginComponents()', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
|
|
||||||
resetLogMock(log);
|
resetLogMock(log);
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = {
|
||||||
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
};
|
||||||
|
|
||||||
jest.mocked(wrapWithPluginContext).mockClear();
|
jest.mocked(wrapWithPluginContext).mockClear();
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import {
|
|||||||
import { useAddedComponentsRegistry } from './ExtensionRegistriesContext';
|
import { useAddedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { isGrafanaDevMode } from './utils';
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
|
import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||||
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||||
|
|
||||||
// Returns an array of component extensions for the given extension point
|
// Returns an array of component extensions for the given extension point
|
||||||
@ -21,6 +22,7 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
const registry = useAddedComponentsRegistry();
|
const registry = useAddedComponentsRegistry();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||||
|
|
||||||
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.
|
||||||
@ -45,6 +47,13 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
components: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
||||||
const { pluginId } = registryItem;
|
const { pluginId } = registryItem;
|
||||||
|
|
||||||
@ -65,5 +74,5 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
components,
|
components,
|
||||||
};
|
};
|
||||||
}, [extensionPointId, limitPerPlugin, pluginContext, registryState]);
|
}, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,11 @@ import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
|||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
import { createUsePluginExtensions } from './usePluginExtensions';
|
import { createUsePluginExtensions } from './usePluginExtensions';
|
||||||
|
|
||||||
|
jest.mock('./useLoadAppPlugins');
|
||||||
|
|
||||||
describe('usePluginExtensions()', () => {
|
describe('usePluginExtensions()', () => {
|
||||||
let registries: PluginExtensionRegistries;
|
let registries: PluginExtensionRegistries;
|
||||||
const pluginId = 'myorg-extensions-app';
|
const pluginId = 'myorg-extensions-app';
|
||||||
@ -18,6 +21,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
};
|
};
|
||||||
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an empty array if there are no extensions registered for the extension point', () => {
|
it('should return an empty array if there are no extensions registered for the extension point', () => {
|
||||||
|
@ -8,7 +8,8 @@ import * as errors from './errors';
|
|||||||
import { getPluginExtensions } from './getPluginExtensions';
|
import { getPluginExtensions } from './getPluginExtensions';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
import { isGrafanaDevMode } from './utils';
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
|
import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||||
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||||
|
|
||||||
export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
|
export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
|
||||||
@ -20,8 +21,9 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries)
|
|||||||
const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
|
const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
|
||||||
const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
|
const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
|
||||||
const { extensionPointId, context, limitPerPlugin } = options;
|
const { extensionPointId, context, limitPerPlugin } = options;
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||||
|
|
||||||
const { extensions } = 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.
|
||||||
const enableRestrictions = isGrafanaDevMode() && pluginContext !== null;
|
const enableRestrictions = isGrafanaDevMode() && pluginContext !== null;
|
||||||
const pluginId = pluginContext?.meta.id ?? '';
|
const pluginId = pluginContext?.meta.id ?? '';
|
||||||
@ -50,19 +52,35 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return getPluginExtensions({
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
extensions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { extensions } = getPluginExtensions({
|
||||||
extensionPointId,
|
extensionPointId,
|
||||||
context,
|
context,
|
||||||
limitPerPlugin,
|
limitPerPlugin,
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { extensions, isLoading: false };
|
||||||
|
|
||||||
// Doing the deps like this instead of just `option` because users probably aren't going to memoize the
|
// Doing the deps like this instead of just `option` because users probably aren't going to memoize the
|
||||||
// options object so we are checking it's simple value attributes.
|
// options object so we are checking it's simple value attributes.
|
||||||
// The context though still has to be memoized though and not mutated.
|
// The context though still has to be memoized though and not mutated.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: refactor `getPluginExtensions` to accept service dependencies as arguments instead of relying on the sidecar singleton under the hood
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: refactor `getPluginExtensions` to accept service dependencies as arguments instead of relying on the sidecar singleton under the hood
|
||||||
}, [addedLinksRegistry, addedComponentsRegistry, extensionPointId, context, limitPerPlugin, pluginContext]);
|
}, [
|
||||||
|
addedLinksRegistry,
|
||||||
return { extensions, isLoading: false };
|
addedComponentsRegistry,
|
||||||
|
extensionPointId,
|
||||||
|
context,
|
||||||
|
limitPerPlugin,
|
||||||
|
pluginContext,
|
||||||
|
isLoadingAppPlugins,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,15 @@ import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
|||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
import { usePluginLinks } from './usePluginLinks';
|
import { usePluginLinks } from './usePluginLinks';
|
||||||
import { isGrafanaDevMode } from './utils';
|
import { isGrafanaDevMode } from './utils';
|
||||||
|
|
||||||
|
jest.mock('./useLoadAppPlugins');
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
id: 'my-app-plugin',
|
id: 'my-app-plugin',
|
||||||
@ -48,8 +52,13 @@ describe('usePluginLinks()', () => {
|
|||||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = {
|
||||||
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
};
|
||||||
resetLogMock(log);
|
resetLogMock(log);
|
||||||
|
|
||||||
pluginMeta = {
|
pluginMeta = {
|
||||||
|
@ -11,8 +11,10 @@ import {
|
|||||||
import { useAddedLinksRegistry } from './ExtensionRegistriesContext';
|
import { useAddedLinksRegistry } from './ExtensionRegistriesContext';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
import {
|
import {
|
||||||
generateExtensionId,
|
generateExtensionId,
|
||||||
|
getExtensionPointPluginDependencies,
|
||||||
getLinkExtensionOnClick,
|
getLinkExtensionOnClick,
|
||||||
getLinkExtensionOverrides,
|
getLinkExtensionOverrides,
|
||||||
getLinkExtensionPathWithTracking,
|
getLinkExtensionPathWithTracking,
|
||||||
@ -30,6 +32,7 @@ export function usePluginLinks({
|
|||||||
const registry = useAddedLinksRegistry();
|
const registry = useAddedLinksRegistry();
|
||||||
const pluginContext = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||||
|
|
||||||
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.
|
||||||
@ -56,6 +59,13 @@ export function usePluginLinks({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!registryState || !registryState[extensionPointId]) {
|
if (!registryState || !registryState[extensionPointId]) {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -117,5 +127,5 @@ export function usePluginLinks({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
links: extensions,
|
links: extensions,
|
||||||
};
|
};
|
||||||
}, [context, extensionPointId, limitPerPlugin, registryState, pluginContext]);
|
}, [context, extensionPointId, limitPerPlugin, registryState, pluginContext, isLoadingAppPlugins]);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { type Unsubscribable } from 'rxjs';
|
import { type Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
import { dateTime, usePluginContext } from '@grafana/data';
|
import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { ShowModalReactEvent } from 'app/types/events';
|
import { ShowModalReactEvent } from 'app/types/events';
|
||||||
|
|
||||||
@ -12,6 +13,10 @@ import {
|
|||||||
getReadOnlyProxy,
|
getReadOnlyProxy,
|
||||||
createOpenModalFunction,
|
createOpenModalFunction,
|
||||||
wrapWithPluginContext,
|
wrapWithPluginContext,
|
||||||
|
getExtensionPointPluginDependencies,
|
||||||
|
getExposedComponentPluginDependencies,
|
||||||
|
getAppPluginConfigs,
|
||||||
|
getAppPluginIdFromExposedComponentId,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
@ -447,4 +452,431 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
|
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAppPluginConfigs()', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const genereicAppPluginConfig = {
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the app plugin configs based on the provided plugin ids', () => {
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([
|
||||||
|
config.apps['myorg-first-app'],
|
||||||
|
config.apps['myorg-third-app'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should simply ignore the app plugin ids that do not belong to a config', () => {
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getAppPluginConfigs(['myorg-first-app', 'unknown-app-id'])).toEqual([config.apps['myorg-first-app']]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAppPluginIdFromExposedComponentId()', () => {
|
||||||
|
test('should return the app plugin id from an extension point id', () => {
|
||||||
|
expect(getAppPluginIdFromExposedComponentId('myorg-extensions-app/component/v1')).toBe('myorg-extensions-app');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExtensionPointPluginDependencies()', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const genereicAppPluginConfig = {
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the app plugin ids that register extensions to a link extension point', () => {
|
||||||
|
const extensionPointId = 'myorg-first-app/link/v1';
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
// This plugin is registering a link extension to the extension point
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [
|
||||||
|
{
|
||||||
|
targets: [extensionPointId],
|
||||||
|
title: 'Link title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual(['myorg-second-app']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the app plugin ids that register extensions to a component extension point', () => {
|
||||||
|
const extensionPointId = 'myorg-first-app/component/v1';
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
},
|
||||||
|
// This plugin is registering a component extension to the extension point
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [
|
||||||
|
{
|
||||||
|
targets: [extensionPointId],
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual(['myorg-third-app']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array if there are no apps that that extend the extension point', () => {
|
||||||
|
const extensionPointId = 'myorg-first-app/component/v1';
|
||||||
|
|
||||||
|
// None of the apps are extending the extension point
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should also return (recursively) the app plugin ids that the apps which extend the extension-point depend on', () => {
|
||||||
|
const extensionPointId = 'myorg-first-app/component/v1';
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
// This plugin is registering a component extension to the extension point.
|
||||||
|
// It is also depending on the 'myorg-fourth-app' plugin.
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [
|
||||||
|
{
|
||||||
|
targets: [extensionPointId],
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
...genereicAppPluginConfig.dependencies,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: ['myorg-fourth-app/component/v1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
// This plugin exposes a component, but is also depending on the 'myorg-fifth-app'.
|
||||||
|
'myorg-fourth-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-fourth-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: 'myorg-fourth-app/component/v1',
|
||||||
|
title: 'Exposed component',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
...genereicAppPluginConfig.dependencies,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: ['myorg-fifth-app/component/v1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-fifth-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-fifth-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: 'myorg-fifth-app/component/v1',
|
||||||
|
title: 'Exposed component',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-sixth-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-sixth-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual(['myorg-second-app', 'myorg-fourth-app', 'myorg-fifth-app']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExposedComponentPluginDependencies()', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const genereicAppPluginConfig = {
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should only return the app plugin id that exposes the component, if that component does not depend on anything', () => {
|
||||||
|
const exposedComponentId = 'myorg-second-app/component/v1';
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: exposedComponentId,
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual(['myorg-second-app']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should also return the list of app plugin ids that the plugin - which exposes the component - is depending on', () => {
|
||||||
|
const exposedComponentId = 'myorg-second-app/component/v1';
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
'myorg-first-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-first-app',
|
||||||
|
},
|
||||||
|
'myorg-second-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-second-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: exposedComponentId,
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
...genereicAppPluginConfig.dependencies,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: ['myorg-fourth-app/component/v1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-third-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-third-app',
|
||||||
|
},
|
||||||
|
'myorg-fourth-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-fourth-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: 'myorg-fourth-app/component/v1',
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
...genereicAppPluginConfig.dependencies,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: ['myorg-fifth-app/component/v1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myorg-fifth-app': {
|
||||||
|
...genereicAppPluginConfig,
|
||||||
|
id: 'myorg-fifth-app',
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [
|
||||||
|
{
|
||||||
|
id: 'myorg-fifth-app/component/v1',
|
||||||
|
title: 'Component title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId);
|
||||||
|
|
||||||
|
expect(appPluginIds).toEqual(['myorg-second-app', 'myorg-fourth-app', 'myorg-fifth-app']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,8 +16,9 @@ import {
|
|||||||
PanelMenuItem,
|
PanelMenuItem,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
|
PluginExtensionPoints,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction, config } from '@grafana/runtime';
|
import { reportInteraction, config, AppPluginConfig } 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';
|
||||||
@ -421,3 +422,75 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string,
|
|||||||
// Comes from the `app_mode` setting in the Grafana config (defaults to "development")
|
// Comes from the `app_mode` setting in the Grafana config (defaults to "development")
|
||||||
// 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) => {
|
||||||
|
return exposedComponentId.split('/')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.)
|
||||||
|
// (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.)
|
||||||
|
export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => {
|
||||||
|
return Object.values(config.apps)
|
||||||
|
.filter(
|
||||||
|
(app) =>
|
||||||
|
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
|
||||||
|
app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId))
|
||||||
|
)
|
||||||
|
.map((app) => app.id)
|
||||||
|
.reduce((acc: string[], id: string) => {
|
||||||
|
return [...acc, id, ...getAppPluginDependencies(id)];
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.)
|
||||||
|
export const getExposedComponentPluginDependencies = (exposedComponentId: string) => {
|
||||||
|
const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId);
|
||||||
|
|
||||||
|
return [pluginId].reduce((acc: string[], pluginId: string) => {
|
||||||
|
return [...acc, pluginId, ...getAppPluginDependencies(pluginId)];
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.)
|
||||||
|
// Heads up! This is a recursive function.
|
||||||
|
export const getAppPluginDependencies = (pluginId: string): string[] => {
|
||||||
|
if (!config.apps[pluginId]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map(
|
||||||
|
getAppPluginIdFromExposedComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
return pluginIdDependencies.reduce((acc, pluginId) => {
|
||||||
|
return [...acc, ...getAppPluginDependencies(pluginId)];
|
||||||
|
}, pluginIdDependencies);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import type { PluginExtensionAddedLinkConfig, PluginExtensionExposedComponentConfig } from '@grafana/data';
|
import type { PluginExtensionAddedLinkConfig, PluginExtensionExposedComponentConfig } from '@grafana/data';
|
||||||
import { PluginExtensionAddedComponentConfig } from '@grafana/data/src/types/pluginExtensions';
|
import { PluginExtensionAddedComponentConfig } from '@grafana/data/src/types/pluginExtensions';
|
||||||
import type { AppPluginConfig } from '@grafana/runtime';
|
import type { AppPluginConfig } from '@grafana/runtime';
|
||||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
|
||||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||||
|
|
||||||
import { PluginExtensionRegistries } from './extensions/registry/types';
|
import { importAppPlugin } from './plugin_loader';
|
||||||
import { importPluginModule } from './plugin_loader';
|
|
||||||
|
|
||||||
export type PluginPreloadResult = {
|
export type PluginPreloadResult = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
@ -15,67 +13,28 @@ export type PluginPreloadResult = {
|
|||||||
addedLinkConfigs?: PluginExtensionAddedLinkConfig[];
|
addedLinkConfigs?: PluginExtensionAddedLinkConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function preloadPlugins(
|
const preloadedAppPlugins = new Set<string>();
|
||||||
apps: AppPluginConfig[] = [],
|
const isNotYetPreloaded = ({ id }: AppPluginConfig) => !preloadedAppPlugins.has(id);
|
||||||
registries: PluginExtensionRegistries,
|
const markAsPreloaded = (apps: AppPluginConfig[]) => apps.forEach(({ id }) => preloadedAppPlugins.add(id));
|
||||||
eventName = 'frontend_plugins_preload'
|
|
||||||
) {
|
|
||||||
startMeasure(eventName);
|
|
||||||
const promises = apps.filter((config) => config.preload).map((config) => preload(config));
|
|
||||||
const preloadedPlugins = await Promise.all(promises);
|
|
||||||
|
|
||||||
for (const preloadedPlugin of preloadedPlugins) {
|
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
||||||
if (preloadedPlugin.error) {
|
const appPluginsToPreload = apps.filter(isNotYetPreloaded);
|
||||||
console.error(`[Plugins] Skip loading extensions for "${preloadedPlugin.pluginId}" due to an error.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
registries.exposedComponentsRegistry.register({
|
if (appPluginsToPreload.length === 0) {
|
||||||
pluginId: preloadedPlugin.pluginId,
|
return;
|
||||||
configs: preloadedPlugin.exposedComponentConfigs,
|
|
||||||
});
|
|
||||||
registries.addedComponentsRegistry.register({
|
|
||||||
pluginId: preloadedPlugin.pluginId,
|
|
||||||
configs: preloadedPlugin.addedComponentConfigs || [],
|
|
||||||
});
|
|
||||||
registries.addedLinksRegistry.register({
|
|
||||||
pluginId: preloadedPlugin.pluginId,
|
|
||||||
configs: preloadedPlugin.addedLinkConfigs || [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopMeasure(eventName);
|
markAsPreloaded(apps);
|
||||||
|
|
||||||
|
await Promise.all(appPluginsToPreload.map(preload));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
async function preload(config: AppPluginConfig) {
|
||||||
const { path, version, id: pluginId, loadingStrategy } = config;
|
|
||||||
try {
|
try {
|
||||||
startMeasure(`frontend_plugin_preload_${pluginId}`);
|
const meta = await getPluginSettings(config.id);
|
||||||
const { plugin } = await importPluginModule({
|
|
||||||
path,
|
|
||||||
version,
|
|
||||||
isAngular: config.angular.detected,
|
|
||||||
pluginId,
|
|
||||||
loadingStrategy,
|
|
||||||
moduleHash: config.moduleHash,
|
|
||||||
});
|
|
||||||
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
|
||||||
|
|
||||||
// Fetching meta-information for the preloaded app plugin and caching it for later.
|
await importAppPlugin(meta);
|
||||||
// (The function below returns a promise, but it's not awaited for a reason: we don't want to block the preload process, we would only like to cache the result for later.)
|
|
||||||
getPluginSettings(pluginId);
|
|
||||||
|
|
||||||
return { pluginId, exposedComponentConfigs, addedComponentConfigs, addedLinkConfigs };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);
|
console.error(`[Plugins] Failed to preload plugin: ${config.path} (version: ${config.version})`, error);
|
||||||
return {
|
|
||||||
pluginId,
|
|
||||||
error,
|
|
||||||
exposedComponentConfigs: [],
|
|
||||||
addedComponentConfigs: [],
|
|
||||||
addedLinkConfigs: [],
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
stopMeasure(`frontend_plugin_preload_${pluginId}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { DataQuery } from '@grafana/schema';
|
|||||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||||
|
|
||||||
import builtInPlugins from './built_in_plugins';
|
import builtInPlugins from './built_in_plugins';
|
||||||
|
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup';
|
||||||
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
||||||
// SystemJS has to be imported before the sharedDependenciesMap
|
// SystemJS has to be imported before the sharedDependenciesMap
|
||||||
import { SystemJS } from './loader/systemjs';
|
import { SystemJS } from './loader/systemjs';
|
||||||
@ -69,6 +70,15 @@ systemJSPrototype.resolve = decorateSystemJSResolve.bind(systemJSPrototype, syst
|
|||||||
// Any css files loaded via SystemJS have their styles applied onload.
|
// Any css files loaded via SystemJS have their styles applied onload.
|
||||||
systemJSPrototype.onload = decorateSystemJsOnload;
|
systemJSPrototype.onload = decorateSystemJsOnload;
|
||||||
|
|
||||||
|
type PluginImportInfo = {
|
||||||
|
path: string;
|
||||||
|
pluginId: string;
|
||||||
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
|
version?: string;
|
||||||
|
isAngular?: boolean;
|
||||||
|
moduleHash?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function importPluginModule({
|
export async function importPluginModule({
|
||||||
path,
|
path,
|
||||||
pluginId,
|
pluginId,
|
||||||
@ -76,14 +86,7 @@ export async function importPluginModule({
|
|||||||
version,
|
version,
|
||||||
isAngular,
|
isAngular,
|
||||||
moduleHash,
|
moduleHash,
|
||||||
}: {
|
}: PluginImportInfo): Promise<System.Module> {
|
||||||
path: string;
|
|
||||||
pluginId: string;
|
|
||||||
loadingStrategy: PluginLoadingStrategy;
|
|
||||||
version?: string;
|
|
||||||
isAngular?: boolean;
|
|
||||||
moduleHash?: string;
|
|
||||||
}): Promise<System.Module> {
|
|
||||||
if (version) {
|
if (version) {
|
||||||
registerPluginInCache({ path, version, loadingStrategy });
|
registerPluginInCache({ path, version, loadingStrategy });
|
||||||
}
|
}
|
||||||
@ -166,21 +169,44 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
// Only successfully loaded plugins are cached
|
||||||
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
const importedAppPlugins: Record<string, AppPlugin> = {};
|
||||||
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
|
||||||
return importPluginModule({
|
export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
||||||
|
const pluginId = meta.id;
|
||||||
|
|
||||||
|
if (importedAppPlugins[pluginId]) {
|
||||||
|
return importedAppPlugins[pluginId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginExports = await importPluginModule({
|
||||||
path: meta.module,
|
path: meta.module,
|
||||||
version: meta.info?.version,
|
version: meta.info?.version,
|
||||||
isAngular,
|
|
||||||
loadingStrategy: fallbackLoadingStrategy,
|
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
|
isAngular: meta.angular?.detected ?? meta.angularDetected,
|
||||||
|
loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch,
|
||||||
moduleHash: meta.moduleHash,
|
moduleHash: meta.moduleHash,
|
||||||
}).then((pluginExports) => {
|
|
||||||
const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin();
|
|
||||||
plugin.init(meta);
|
|
||||||
plugin.meta = meta;
|
|
||||||
plugin.setComponentsFromLegacyExports(pluginExports);
|
|
||||||
return plugin;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { plugin = new AppPlugin() } = pluginExports;
|
||||||
|
plugin.init(meta);
|
||||||
|
plugin.meta = meta;
|
||||||
|
plugin.setComponentsFromLegacyExports(pluginExports);
|
||||||
|
|
||||||
|
exposedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: plugin.exposedComponentConfigs || [],
|
||||||
|
});
|
||||||
|
addedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: plugin.addedComponentConfigs || [],
|
||||||
|
});
|
||||||
|
addedLinksRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: plugin.addedLinkConfigs || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
importedAppPlugins[pluginId] = plugin;
|
||||||
|
|
||||||
|
return plugin;
|
||||||
}
|
}
|
||||||
|
@ -12,27 +12,25 @@ jest.mock('app/core/core', () => {
|
|||||||
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
|
import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data';
|
||||||
|
|
||||||
// Loaded after the `unmock` above
|
// Loaded after the `unmock` above
|
||||||
|
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from '../extensions/registry/setup';
|
||||||
import { SystemJS } from '../loader/systemjs';
|
import { SystemJS } from '../loader/systemjs';
|
||||||
import { importAppPlugin } from '../plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
|
|
||||||
class MyCustomApp extends AppPlugin {
|
jest.mock('../extensions/registry/setup');
|
||||||
initWasCalled = false;
|
|
||||||
calledTwice = false;
|
|
||||||
|
|
||||||
init(meta: AppPluginMeta) {
|
|
||||||
this.initWasCalled = true;
|
|
||||||
this.calledTwice = this.meta === meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Load App', () => {
|
describe('Load App', () => {
|
||||||
const app = new MyCustomApp();
|
const app = new AppPlugin();
|
||||||
const modulePath = 'http://localhost:3000/public/plugins/my-app-plugin/module.js';
|
const modulePath = 'http://localhost:3000/public/plugins/my-app-plugin/module.js';
|
||||||
// Hook resolver for tests
|
// Hook resolver for tests
|
||||||
const originalResolve = SystemJS.constructor.prototype.resolve;
|
const originalResolve = SystemJS.constructor.prototype.resolve;
|
||||||
SystemJS.constructor.prototype.resolve = (x: unknown) => x;
|
SystemJS.constructor.prototype.resolve = (x: unknown) => x;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
app.init = jest.fn();
|
||||||
|
addedComponentsRegistry.register = jest.fn();
|
||||||
|
addedLinksRegistry.register = jest.fn();
|
||||||
|
exposedComponentsRegistry.register = jest.fn();
|
||||||
|
|
||||||
SystemJS.set(modulePath, { plugin: app });
|
SystemJS.set(modulePath, { plugin: app });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,14 +53,17 @@ describe('Load App', () => {
|
|||||||
const m = await SystemJS.import(modulePath);
|
const m = await SystemJS.import(modulePath);
|
||||||
expect(m.plugin).toBe(app);
|
expect(m.plugin).toBe(app);
|
||||||
|
|
||||||
const loaded = await importAppPlugin(meta);
|
// Importing the app should initialise the meta
|
||||||
expect(loaded).toBe(app);
|
const importedApp = await importAppPlugin(meta);
|
||||||
|
expect(importedApp).toBe(app);
|
||||||
expect(app.meta).toBe(meta);
|
expect(app.meta).toBe(meta);
|
||||||
expect(app.initWasCalled).toBeTruthy();
|
|
||||||
expect(app.calledTwice).toBeFalsy();
|
|
||||||
|
|
||||||
const again = await importAppPlugin(meta);
|
// Importing the same app again doesn't initialise it twice
|
||||||
expect(again).toBe(app);
|
const importedAppAgain = await importAppPlugin(meta);
|
||||||
expect(app.calledTwice).toBeTruthy();
|
expect(importedAppAgain).toBe(app);
|
||||||
|
expect(app.init).toHaveBeenCalledTimes(1);
|
||||||
|
expect(addedComponentsRegistry.register).toHaveBeenCalledTimes(1);
|
||||||
|
expect(addedLinksRegistry.register).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exposedComponentsRegistry.register).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user