Extensions: Wrap extension components with a sandbox wrapper div (#103064)
* feat(sandbox): wrap extension components with sandbox wrapper div * fix: remove circular dependency This was caused by sandbox_plugin_loader_registry -> helpers -> backend_srv -> ... plugin_loader -> setup. * fix: add dependency to react hook * tests: add tests for extensions sandbox wrapper * fix: only wrap modal content after plugin loading * chore: remove unused code * Wip * review(actions.ts): extract logic to function * test: remove testing app id * tests: fix extension utils tests * tests: remove unnecessary code
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<number, []>;
|
||||
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',
|
||||
|
||||
@@ -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<Promise<PanelPlugin>> =
|
||||
return plugin;
|
||||
};
|
||||
};
|
||||
|
||||
function updatePanels() {
|
||||
return getBackendSrv()
|
||||
.get('/api/frontend/settings')
|
||||
.then((settings: Settings) => {
|
||||
config.panels = settings.panels;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: () => <div>Text in body</div>,
|
||||
});
|
||||
|
||||
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({
|
||||
|
||||
@@ -133,7 +133,9 @@ const getModalWrapper = ({
|
||||
fallbackAlwaysVisible={true}
|
||||
log={baseLog}
|
||||
>
|
||||
<Body onDismiss={onDismiss} />
|
||||
<div data-plugin-sandbox={config.pluginId} data-testid="plugin-sandbox-wrapper">
|
||||
<Body onDismiss={onDismiss} />
|
||||
</div>
|
||||
</ExtensionErrorBoundary>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -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<GrafanaPlugin['configPages']> = Reflect.get(pluginObject, 'configPages') ?? [];
|
||||
|
||||
Reference in New Issue
Block a user