diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 38660fba145..b13716b8ca2 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -2,9 +2,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data'; import { config, featureEnabled } from '@grafana/runtime'; -import { Settings } from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { getBackendSrv } from 'app/core/services/backend_srv'; import { AccessControlAction } from 'app/types'; import { @@ -341,14 +339,6 @@ function getPluginSignature(options: { return PluginSignatureStatus.missing; } -// Updates the core Grafana config to have the correct list available panels -export const updatePanels = () => - getBackendSrv() - .get('/api/frontend/settings') - .then((settings: Settings) => { - config.panels = settings.panels; - }); - export function getLatestCompatibleVersion(versions: Version[] | undefined): Version | undefined { if (!versions) { return; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 42b83c85835..b725d3a9d58 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { GrafanaEdition } from '@grafana/data/internal'; import { selectors } from '@grafana/e2e-selectors'; -import { config } from '@grafana/runtime'; +import { config, getBackendSrv, setBackendSrv } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import * as api from '../api'; @@ -30,10 +30,10 @@ import { import PluginDetailsPage from './PluginDetails'; jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - const mockedRuntime = { ...original }; - mockedRuntime.config.buildInfo.version = 'v8.1.0'; - return mockedRuntime; + const runtime = jest.requireActual('@grafana/runtime'); + runtime.config.buildInfo.version = 'v8.1.0'; + + return runtime; }); jest.mock('../hooks/usePluginConfig.tsx', () => ({ @@ -44,11 +44,6 @@ jest.mock('../hooks/usePluginConfig.tsx', () => ({ })), })); -jest.mock('../helpers.ts', () => ({ - ...jest.requireActual('../helpers.ts'), - updatePanels: jest.fn(), -})); - jest.mock('app/core/core', () => ({ contextSrv: { hasPermission: (action: string) => true, @@ -85,6 +80,7 @@ describe('Plugin details page', () => { const id = 'my-plugin'; const originalWindowLocation = window.location; let dateNow: jest.SpyInstance; + const originalBackendSrv = getBackendSrv(); beforeAll(() => { dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00 @@ -100,6 +96,7 @@ describe('Plugin details page', () => { jest.clearAllMocks(); config.pluginAdminExternalManageEnabled = false; config.licenseInfo.enabledFeatures = {}; + setBackendSrv(originalBackendSrv); }); afterAll(() => { @@ -422,6 +419,11 @@ describe('Plugin details page', () => { // @ts-ignore api.uninstallPlugin = jest.fn(); + setBackendSrv({ + ...originalBackendSrv, + get: jest.fn().mockResolvedValue({ panels: [] }), + }); + const { queryByText, getByRole, findByRole, user } = renderPluginDetails({ id, name: 'Akumuli', diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index e08e7241c42..6f2c5400cf0 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -3,6 +3,7 @@ import { from, forkJoin, timeout, lastValueFrom, catchError, of } from 'rxjs'; import { PanelPlugin, PluginError } from '@grafana/data'; import { config, getBackendSrv, isFetchError } from '@grafana/runtime'; +import { Settings } from 'app/core/config'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { StoreState, ThunkResult } from 'app/types'; @@ -18,7 +19,7 @@ import { getProvisionedPlugins, } from '../api'; import { STATE_PREFIX } from '../constants'; -import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers'; +import { mapLocalToCatalog, mergeLocalsAndRemotes } from '../helpers'; import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin, PluginStatus } from '../types'; // Fetches @@ -278,3 +279,11 @@ export const loadPanelPlugin = (id: string): ThunkResult> = return plugin; }; }; + +function updatePanels() { + return getBackendSrv() + .get('/api/frontend/settings') + .then((settings: Settings) => { + config.panels = settings.panels; + }); +} diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index e3b74d8cd1a..a99df507d1b 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -7,6 +7,7 @@ import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, @@ -26,7 +27,7 @@ import { jest.mock('app/features/plugins/pluginSettings', () => ({ ...jest.requireActual('app/features/plugins/pluginSettings'), - getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }), + getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' }, id: 'test-plugin' }), })); describe('Plugin Extensions / Utils', () => { @@ -36,6 +37,9 @@ describe('Plugin Extensions / Utils', () => { jest.spyOn(log, 'error').mockImplementation(() => {}); jest.spyOn(log, 'warning').mockImplementation(() => {}); jest.spyOn(log, 'debug').mockImplementation(() => {}); + jest.spyOn(log, 'info').mockImplementation(() => {}); + jest.spyOn(log, 'trace').mockImplementation(() => {}); + jest.spyOn(log, 'fatal').mockImplementation(() => {}); }); afterEach(() => { @@ -723,6 +727,27 @@ describe('Plugin Extensions / Utils', () => { expect(modal).toHaveTextContent('Version: 1.0.0'); }); + it('should add a wrapper div with a "data-plugin-sandbox" attribute', async () => { + const pluginId = 'grafana-worldmap-panel'; + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); + + openModal({ + title: 'Title in modal', + body: () =>
Text in body
, + }); + + expect(await screen.findByRole('dialog')).toBeVisible(); + + expect(screen.getByTestId('plugin-sandbox-wrapper')).toHaveAttribute( + 'data-plugin-sandbox', + 'grafana-worldmap-panel' + ); + }); + it('should show an error alert in the modal IN DEV MODE if the extension throws an error', async () => { config.buildInfo.env = 'development'; jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -816,6 +841,10 @@ describe('Plugin Extensions / Utils', () => { ); }; + beforeEach(() => { + resetLogMock(log); + }); + it('should make the plugin context available for the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; const Component = wrapWithPluginContext({ diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 23d21c11058..63a2d2bfc83 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -133,7 +133,9 @@ const getModalWrapper = ({ fallbackAlwaysVisible={true} log={baseLog} > - +
+ +
); diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandbox_components.tsx index 7e11e8428bc..29205b06bc0 100644 --- a/public/app/features/plugins/sandbox/sandbox_components.tsx +++ b/public/app/features/plugins/sandbox/sandbox_components.tsx @@ -2,7 +2,12 @@ import { isFunction } from 'lodash'; import { ComponentType, FC } from 'react'; import * as React from 'react'; -import { GrafanaPlugin, PluginType } from '@grafana/data'; +import { + GrafanaPlugin, + PluginExtensionAddedComponentConfig, + PluginExtensionExposedComponentConfig, + PluginType, +} from '@grafana/data'; import { SandboxPluginMeta, SandboxedPluginObject } from './types'; import { isSandboxedPluginObject } from './utils'; @@ -58,6 +63,35 @@ export async function sandboxPluginComponents( Reflect.set(pluginObject, 'root', withSandboxWrapper(Reflect.get(pluginObject, 'root'), meta)); } + // Extensions: added components + if (Reflect.has(pluginObject, 'addedComponentConfigs')) { + const addedComponents: PluginExtensionAddedComponentConfig[] = Reflect.get(pluginObject, 'addedComponentConfigs'); + for (const addedComponent of addedComponents) { + if (Reflect.has(addedComponent, 'component')) { + Reflect.set(addedComponent, 'component', withSandboxWrapper(Reflect.get(addedComponent, 'component'), meta)); + } + } + Reflect.set(pluginObject, 'addedComponentConfigs', addedComponents); + } + + // Extensions: exposed components + if (Reflect.has(pluginObject, 'exposedComponentConfigs')) { + const exposedComponents: PluginExtensionExposedComponentConfig[] = Reflect.get( + pluginObject, + 'exposedComponentConfigs' + ); + for (const exposedComponent of exposedComponents) { + if (Reflect.has(exposedComponent, 'component')) { + Reflect.set( + exposedComponent, + 'component', + withSandboxWrapper(Reflect.get(exposedComponent, 'component'), meta) + ); + } + } + Reflect.set(pluginObject, 'exposedComponentConfigs', exposedComponents); + } + // config pages if (Reflect.has(pluginObject, 'configPages')) { const configPages: NonNullable = Reflect.get(pluginObject, 'configPages') ?? [];