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
|
||||
|
||||
- `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 { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||
import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext';
|
||||
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { ExperimentalSplitPaneRouterWrapper, RouterWrapper } from './routes/RoutesWrapper';
|
||||
|
||||
interface AppWrapperProps {
|
||||
@ -104,7 +105,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
|
||||
<GlobalStyles />
|
||||
<MaybeTimeRangeProvider>
|
||||
<SidecarContext_EXPERIMENTAL.Provider value={sidecarServiceSingleton_EXPERIMENTAL}>
|
||||
<ExtensionRegistriesProvider registries={app.pluginExtensionsRegistries}>
|
||||
<ExtensionRegistriesProvider registries={pluginExtensionRegistries}>
|
||||
<div className="grafana-app">
|
||||
{config.featureToggles.appSidecar ? (
|
||||
<ExperimentalSplitPaneRouterWrapper {...routerWrapperProps} />
|
||||
|
@ -85,12 +85,12 @@ import { PanelDataErrorView } from './features/panel/components/PanelDataErrorVi
|
||||
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
||||
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
||||
import { createPluginExtensionsGetter } from './features/plugins/extensions/getPluginExtensions';
|
||||
import { setupPluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { PluginExtensionRegistries } from './features/plugins/extensions/registry/types';
|
||||
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||
@ -127,7 +127,6 @@ if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
export class GrafanaApp {
|
||||
context!: GrafanaContextType;
|
||||
pluginExtensionsRegistries!: PluginExtensionRegistries;
|
||||
|
||||
async init() {
|
||||
try {
|
||||
@ -217,22 +216,16 @@ export class GrafanaApp {
|
||||
setDataSourceSrv(dataSourceSrv);
|
||||
initWindowRuntime();
|
||||
|
||||
// Initialize plugin extensions
|
||||
this.pluginExtensionsRegistries = setupPluginExtensionRegistries();
|
||||
|
||||
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.
|
||||
// TODO: remove the following exception once the issue mentioned above is fixed.
|
||||
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));
|
||||
const appPluginsToAwait = getAppPluginsToAwait();
|
||||
const appPluginsToPreload = getAppPluginsToPreload();
|
||||
|
||||
preloadPlugins(appPlugins, this.pluginExtensionsRegistries);
|
||||
await preloadPlugins(awaitedAppPlugins, this.pluginExtensionsRegistries, 'frontend_awaited_plugins_preload');
|
||||
preloadPlugins(appPluginsToPreload);
|
||||
await preloadPlugins(appPluginsToAwait);
|
||||
}
|
||||
|
||||
setPluginExtensionGetter(createPluginExtensionsGetter(this.pluginExtensionsRegistries));
|
||||
setPluginExtensionsHook(createUsePluginExtensions(this.pluginExtensionsRegistries));
|
||||
setPluginExtensionGetter(createPluginExtensionsGetter(pluginExtensionRegistries));
|
||||
setPluginExtensionsHook(createUsePluginExtensions(pluginExtensionRegistries));
|
||||
setPluginLinksHook(usePluginLinks);
|
||||
setPluginComponentHook(usePluginComponent);
|
||||
setPluginComponentsHook(usePluginComponents);
|
||||
|
@ -11,7 +11,9 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
|
||||
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 { importAppPlugin } from '../plugin_loader';
|
||||
|
||||
@ -86,7 +88,11 @@ function renderUnderRouter(page = '') {
|
||||
|
||||
appPluginNavItem.parentItem = appsSection;
|
||||
|
||||
const registries = setupPluginExtensionRegistries();
|
||||
const registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
};
|
||||
const pagePath = page ? `/${page}` : '';
|
||||
const route = {
|
||||
path: `/a/:pluginId/*`,
|
||||
|
@ -5,17 +5,17 @@ import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './types';
|
||||
|
||||
export function setupPluginExtensionRegistries(): PluginExtensionRegistries {
|
||||
const pluginExtensionsRegistries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
};
|
||||
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||
export const addedLinksRegistry = new AddedLinksRegistry();
|
||||
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
||||
addedComponentsRegistry,
|
||||
exposedComponentsRegistry,
|
||||
addedLinksRegistry,
|
||||
};
|
||||
|
||||
pluginExtensionsRegistries.addedLinksRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: getCoreExtensionConfigurations(),
|
||||
});
|
||||
|
||||
return pluginExtensionsRegistries;
|
||||
}
|
||||
// Registering core extensions
|
||||
addedLinksRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: getCoreExtensionConfigurations(),
|
||||
});
|
||||
|
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 { log } from './logs/log';
|
||||
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 { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginComponent } from './usePluginComponent';
|
||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||
|
||||
jest.mock('./useLoadAppPlugins');
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn().mockResolvedValue({
|
||||
id: 'my-app-plugin',
|
||||
@ -83,7 +87,12 @@ describe('usePluginComponent()', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
registries = setupPluginExtensionRegistries();
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
};
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
resetLogMock(log);
|
||||
|
||||
|
@ -7,7 +7,8 @@ import { UsePluginComponentResult } from '@grafana/runtime';
|
||||
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
import { log } from './logs/log';
|
||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { getExposedComponentPluginDependencies, isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||
import { isExposedComponentDependencyMissing } from './validators';
|
||||
|
||||
// Returns a component exposed by a plugin.
|
||||
@ -16,11 +17,19 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl
|
||||
const registry = useExposedComponentsRegistry();
|
||||
const registryState = useObservable(registry.asObservable());
|
||||
const pluginContext = usePluginContext();
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id));
|
||||
|
||||
return useMemo(() => {
|
||||
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
||||
const enableRestrictions = isGrafanaDevMode() && pluginContext;
|
||||
|
||||
if (isLoadingAppPlugins) {
|
||||
return {
|
||||
isLoading: true,
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!registryState?.[id]) {
|
||||
return {
|
||||
isLoading: false,
|
||||
@ -47,5 +56,5 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl
|
||||
isLoading: false,
|
||||
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 { log } from './logs/log';
|
||||
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 { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginComponents } from './usePluginComponents';
|
||||
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||
|
||||
jest.mock('./useLoadAppPlugins');
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn().mockResolvedValue({
|
||||
id: 'my-app-plugin',
|
||||
@ -50,8 +54,14 @@ describe('usePluginComponents()', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
|
||||
resetLogMock(log);
|
||||
registries = setupPluginExtensionRegistries();
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
};
|
||||
|
||||
jest.mocked(wrapWithPluginContext).mockClear();
|
||||
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
import { useAddedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
import { log } from './logs/log';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||
|
||||
// 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 registryState = useObservable(registry.asObservable());
|
||||
const pluginContext = usePluginContext();
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||
|
||||
return useMemo(() => {
|
||||
// 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] ?? []) {
|
||||
const { pluginId } = registryItem;
|
||||
|
||||
@ -65,5 +74,5 @@ export function usePluginComponents<Props extends object = {}>({
|
||||
isLoading: false,
|
||||
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 { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { createUsePluginExtensions } from './usePluginExtensions';
|
||||
|
||||
jest.mock('./useLoadAppPlugins');
|
||||
|
||||
describe('usePluginExtensions()', () => {
|
||||
let registries: PluginExtensionRegistries;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
@ -18,6 +21,7 @@ describe('usePluginExtensions()', () => {
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
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', () => {
|
||||
|
@ -8,7 +8,8 @@ import * as errors from './errors';
|
||||
import { getPluginExtensions } from './getPluginExtensions';
|
||||
import { log } from './logs/log';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||
|
||||
export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
|
||||
@ -20,8 +21,9 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries)
|
||||
const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
|
||||
const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
|
||||
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.
|
||||
const enableRestrictions = isGrafanaDevMode() && pluginContext !== null;
|
||||
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,
|
||||
context,
|
||||
limitPerPlugin,
|
||||
addedComponentsRegistry,
|
||||
addedLinksRegistry,
|
||||
});
|
||||
|
||||
return { extensions, isLoading: false };
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
}, [addedLinksRegistry, addedComponentsRegistry, extensionPointId, context, limitPerPlugin, pluginContext]);
|
||||
|
||||
return { extensions, isLoading: false };
|
||||
}, [
|
||||
addedLinksRegistry,
|
||||
addedComponentsRegistry,
|
||||
extensionPointId,
|
||||
context,
|
||||
limitPerPlugin,
|
||||
pluginContext,
|
||||
isLoadingAppPlugins,
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
@ -6,11 +6,15 @@ import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import { log } from './logs/log';
|
||||
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 { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginLinks } from './usePluginLinks';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
|
||||
jest.mock('./useLoadAppPlugins');
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn().mockResolvedValue({
|
||||
id: 'my-app-plugin',
|
||||
@ -48,8 +52,13 @@ describe('usePluginLinks()', () => {
|
||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
registries = setupPluginExtensionRegistries();
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
};
|
||||
resetLogMock(log);
|
||||
|
||||
pluginMeta = {
|
||||
|
@ -11,8 +11,10 @@ import {
|
||||
import { useAddedLinksRegistry } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
import { log } from './logs/log';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import {
|
||||
generateExtensionId,
|
||||
getExtensionPointPluginDependencies,
|
||||
getLinkExtensionOnClick,
|
||||
getLinkExtensionOverrides,
|
||||
getLinkExtensionPathWithTracking,
|
||||
@ -30,6 +32,7 @@ export function usePluginLinks({
|
||||
const registry = useAddedLinksRegistry();
|
||||
const pluginContext = usePluginContext();
|
||||
const registryState = useObservable(registry.asObservable());
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||
|
||||
return useMemo(() => {
|
||||
// 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]) {
|
||||
return {
|
||||
isLoading: false,
|
||||
@ -117,5 +127,5 @@ export function usePluginLinks({
|
||||
isLoading: false,
|
||||
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 { 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 { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
@ -12,6 +13,10 @@ import {
|
||||
getReadOnlyProxy,
|
||||
createOpenModalFunction,
|
||||
wrapWithPluginContext,
|
||||
getExtensionPointPluginDependencies,
|
||||
getExposedComponentPluginDependencies,
|
||||
getAppPluginConfigs,
|
||||
getAppPluginIdFromExposedComponentId,
|
||||
} from './utils';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
@ -447,4 +452,431 @@ describe('Plugin Extensions / Utils', () => {
|
||||
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,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
urlUtil,
|
||||
PluginExtensionPoints,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
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")
|
||||
// Can be set with the `GF_DEFAULT_APP_MODE` environment variable
|
||||
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 { PluginExtensionAddedComponentConfig } from '@grafana/data/src/types/pluginExtensions';
|
||||
import type { AppPluginConfig } from '@grafana/runtime';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { PluginExtensionRegistries } from './extensions/registry/types';
|
||||
import { importPluginModule } from './plugin_loader';
|
||||
import { importAppPlugin } from './plugin_loader';
|
||||
|
||||
export type PluginPreloadResult = {
|
||||
pluginId: string;
|
||||
@ -15,67 +13,28 @@ export type PluginPreloadResult = {
|
||||
addedLinkConfigs?: PluginExtensionAddedLinkConfig[];
|
||||
};
|
||||
|
||||
export async function preloadPlugins(
|
||||
apps: AppPluginConfig[] = [],
|
||||
registries: PluginExtensionRegistries,
|
||||
eventName = 'frontend_plugins_preload'
|
||||
) {
|
||||
startMeasure(eventName);
|
||||
const promises = apps.filter((config) => config.preload).map((config) => preload(config));
|
||||
const preloadedPlugins = await Promise.all(promises);
|
||||
const preloadedAppPlugins = new Set<string>();
|
||||
const isNotYetPreloaded = ({ id }: AppPluginConfig) => !preloadedAppPlugins.has(id);
|
||||
const markAsPreloaded = (apps: AppPluginConfig[]) => apps.forEach(({ id }) => preloadedAppPlugins.add(id));
|
||||
|
||||
for (const preloadedPlugin of preloadedPlugins) {
|
||||
if (preloadedPlugin.error) {
|
||||
console.error(`[Plugins] Skip loading extensions for "${preloadedPlugin.pluginId}" due to an error.`);
|
||||
continue;
|
||||
}
|
||||
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
||||
const appPluginsToPreload = apps.filter(isNotYetPreloaded);
|
||||
|
||||
registries.exposedComponentsRegistry.register({
|
||||
pluginId: preloadedPlugin.pluginId,
|
||||
configs: preloadedPlugin.exposedComponentConfigs,
|
||||
});
|
||||
registries.addedComponentsRegistry.register({
|
||||
pluginId: preloadedPlugin.pluginId,
|
||||
configs: preloadedPlugin.addedComponentConfigs || [],
|
||||
});
|
||||
registries.addedLinksRegistry.register({
|
||||
pluginId: preloadedPlugin.pluginId,
|
||||
configs: preloadedPlugin.addedLinkConfigs || [],
|
||||
});
|
||||
if (appPluginsToPreload.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopMeasure(eventName);
|
||||
markAsPreloaded(apps);
|
||||
|
||||
await Promise.all(appPluginsToPreload.map(preload));
|
||||
}
|
||||
|
||||
async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
||||
const { path, version, id: pluginId, loadingStrategy } = config;
|
||||
async function preload(config: AppPluginConfig) {
|
||||
try {
|
||||
startMeasure(`frontend_plugin_preload_${pluginId}`);
|
||||
const { plugin } = await importPluginModule({
|
||||
path,
|
||||
version,
|
||||
isAngular: config.angular.detected,
|
||||
pluginId,
|
||||
loadingStrategy,
|
||||
moduleHash: config.moduleHash,
|
||||
});
|
||||
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
||||
const meta = await getPluginSettings(config.id);
|
||||
|
||||
// Fetching meta-information for the preloaded app plugin and caching it for later.
|
||||
// (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 };
|
||||
await importAppPlugin(meta);
|
||||
} catch (error) {
|
||||
console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);
|
||||
return {
|
||||
pluginId,
|
||||
error,
|
||||
exposedComponentConfigs: [],
|
||||
addedComponentConfigs: [],
|
||||
addedLinkConfigs: [],
|
||||
};
|
||||
} finally {
|
||||
stopMeasure(`frontend_plugin_preload_${pluginId}`);
|
||||
console.error(`[Plugins] Failed to preload plugin: ${config.path} (version: ${config.version})`, error);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { DataQuery } from '@grafana/schema';
|
||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||
|
||||
import builtInPlugins from './built_in_plugins';
|
||||
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup';
|
||||
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
||||
// SystemJS has to be imported before the sharedDependenciesMap
|
||||
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.
|
||||
systemJSPrototype.onload = decorateSystemJsOnload;
|
||||
|
||||
type PluginImportInfo = {
|
||||
path: string;
|
||||
pluginId: string;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
version?: string;
|
||||
isAngular?: boolean;
|
||||
moduleHash?: string;
|
||||
};
|
||||
|
||||
export async function importPluginModule({
|
||||
path,
|
||||
pluginId,
|
||||
@ -76,14 +86,7 @@ export async function importPluginModule({
|
||||
version,
|
||||
isAngular,
|
||||
moduleHash,
|
||||
}: {
|
||||
path: string;
|
||||
pluginId: string;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
version?: string;
|
||||
isAngular?: boolean;
|
||||
moduleHash?: string;
|
||||
}): Promise<System.Module> {
|
||||
}: PluginImportInfo): Promise<System.Module> {
|
||||
if (version) {
|
||||
registerPluginInCache({ path, version, loadingStrategy });
|
||||
}
|
||||
@ -166,21 +169,44 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
||||
});
|
||||
}
|
||||
|
||||
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
||||
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
||||
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
||||
return importPluginModule({
|
||||
// Only successfully loaded plugins are cached
|
||||
const importedAppPlugins: Record<string, AppPlugin> = {};
|
||||
|
||||
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,
|
||||
version: meta.info?.version,
|
||||
isAngular,
|
||||
loadingStrategy: fallbackLoadingStrategy,
|
||||
pluginId: meta.id,
|
||||
isAngular: meta.angular?.detected ?? meta.angularDetected,
|
||||
loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch,
|
||||
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';
|
||||
|
||||
// Loaded after the `unmock` above
|
||||
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from '../extensions/registry/setup';
|
||||
import { SystemJS } from '../loader/systemjs';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
|
||||
class MyCustomApp extends AppPlugin {
|
||||
initWasCalled = false;
|
||||
calledTwice = false;
|
||||
|
||||
init(meta: AppPluginMeta) {
|
||||
this.initWasCalled = true;
|
||||
this.calledTwice = this.meta === meta;
|
||||
}
|
||||
}
|
||||
jest.mock('../extensions/registry/setup');
|
||||
|
||||
describe('Load App', () => {
|
||||
const app = new MyCustomApp();
|
||||
const app = new AppPlugin();
|
||||
const modulePath = 'http://localhost:3000/public/plugins/my-app-plugin/module.js';
|
||||
// Hook resolver for tests
|
||||
const originalResolve = SystemJS.constructor.prototype.resolve;
|
||||
SystemJS.constructor.prototype.resolve = (x: unknown) => x;
|
||||
|
||||
beforeAll(() => {
|
||||
app.init = jest.fn();
|
||||
addedComponentsRegistry.register = jest.fn();
|
||||
addedLinksRegistry.register = jest.fn();
|
||||
exposedComponentsRegistry.register = jest.fn();
|
||||
|
||||
SystemJS.set(modulePath, { plugin: app });
|
||||
});
|
||||
|
||||
@ -55,14 +53,17 @@ describe('Load App', () => {
|
||||
const m = await SystemJS.import(modulePath);
|
||||
expect(m.plugin).toBe(app);
|
||||
|
||||
const loaded = await importAppPlugin(meta);
|
||||
expect(loaded).toBe(app);
|
||||
// Importing the app should initialise the meta
|
||||
const importedApp = await importAppPlugin(meta);
|
||||
expect(importedApp).toBe(app);
|
||||
expect(app.meta).toBe(meta);
|
||||
expect(app.initWasCalled).toBeTruthy();
|
||||
expect(app.calledTwice).toBeFalsy();
|
||||
|
||||
const again = await importAppPlugin(meta);
|
||||
expect(again).toBe(app);
|
||||
expect(app.calledTwice).toBeTruthy();
|
||||
// Importing the same app again doesn't initialise it twice
|
||||
const importedAppAgain = await importAppPlugin(meta);
|
||||
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