mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Datasources: Make the datasources config extendable by plugins (#68064)
* feat: add a new UI extension type: component * fix: remove reference to not existing type * chore: update betterer results * review: use a single type notation in import * review: stop exporting `PluginExtensionBase` * refactor: make extension config types more explicit By using some repetition now these types are much easier to oversee. * feat: add a new extension point to the datasources config * fix: export tcontext type from grafana-data * chore: update betterer results * chore: fix tests * feat: extend the context shared with extensions * feat: stop omitting jsonData props & update context type * tests: update tests
This commit is contained in:
@@ -2,8 +2,8 @@ import { screen, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { PluginState } from '@grafana/data';
|
||||
import { setAngularLoader } from '@grafana/runtime';
|
||||
import { PluginExtensionTypes, PluginState } from '@grafana/data';
|
||||
import { setAngularLoader, setPluginExtensionGetter } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { getMockDataSource, getMockDataSourceMeta, getMockDataSourceSettingsState } from '../__mocks__';
|
||||
@@ -13,9 +13,8 @@ import { readOnlyMessage } from './DataSourceReadOnlyMessage';
|
||||
import { EditDataSourceView, ViewProps } from './EditDataSource';
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const original = jest.requireActual('@grafana/runtime');
|
||||
return {
|
||||
...original,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: jest.fn(() => ({
|
||||
getInstanceSettings: (uid: string) => ({
|
||||
uid,
|
||||
@@ -59,6 +58,10 @@ describe('<EditDataSource>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setPluginExtensionGetter(jest.fn().mockReturnValue({ extensions: [] }));
|
||||
});
|
||||
|
||||
describe('On loading errors', () => {
|
||||
it('should render a Back button', () => {
|
||||
setup({
|
||||
@@ -261,4 +264,101 @@ describe('<EditDataSource>', () => {
|
||||
expect(screen.queryByText(detailsVerboseMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when extending the datasource config form', () => {
|
||||
it('should be possible to extend the form with a "component" extension in case the plugin ID is whitelisted', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
|
||||
setPluginExtensionGetter(
|
||||
jest.fn().mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
id: '1',
|
||||
pluginId: 'grafana-pdc-app',
|
||||
type: PluginExtensionTypes.component,
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component: () => <div>{message}</div>,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
setup({
|
||||
dataSourceRights: {
|
||||
readOnly: false,
|
||||
hasDeleteRights: true,
|
||||
hasWriteRights: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText(message)).toBeVisible();
|
||||
});
|
||||
|
||||
it('should NOT be possible to extend the form with a "component" extension in case the plugin ID is NOT whitelisted', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
|
||||
setPluginExtensionGetter(
|
||||
jest.fn().mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
id: '1',
|
||||
pluginId: 'myorg-basic-app',
|
||||
type: PluginExtensionTypes.component,
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component: () => <div>{message}</div>,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
setup({
|
||||
dataSourceRights: {
|
||||
readOnly: false,
|
||||
hasDeleteRights: true,
|
||||
hasWriteRights: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText(message)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass a context prop to the rendered UI extension component', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
const component = jest.fn().mockReturnValue(<div>{message}</div>);
|
||||
|
||||
setPluginExtensionGetter(
|
||||
jest.fn().mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
id: '1',
|
||||
pluginId: 'grafana-pdc-app',
|
||||
type: PluginExtensionTypes.component,
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
setup({
|
||||
dataSourceRights: {
|
||||
readOnly: false,
|
||||
hasDeleteRights: true,
|
||||
hasWriteRights: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component).toHaveBeenCalled();
|
||||
|
||||
const props = component.mock.calls[0][0];
|
||||
|
||||
expect(props.context).toBeDefined();
|
||||
expect(props.context.dataSource).toBeDefined();
|
||||
expect(props.context.dataSourceMeta).toBeDefined();
|
||||
expect(props.context.setJsonData).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
DataSourcePluginContextProvider,
|
||||
DataSourcePluginMeta,
|
||||
DataSourceSettings as DataSourceSettingsType,
|
||||
PluginExtensionPoints,
|
||||
PluginExtensionDataSourceConfigContext,
|
||||
DataSourceJsonData,
|
||||
DataSourceUpdatedSuccessfully,
|
||||
} from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { getDataSourceSrv, getPluginComponentExtensions } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { DataSourceSettingsState, useDispatch } from 'app/types';
|
||||
|
||||
@@ -125,6 +131,7 @@ export function EditDataSourceView({
|
||||
try {
|
||||
await onUpdate({ ...dataSource });
|
||||
trackDsConfigUpdated('success');
|
||||
appEvents.publish(new DataSourceUpdatedSuccessfully());
|
||||
} catch (err) {
|
||||
trackDsConfigUpdated('fail');
|
||||
return;
|
||||
@@ -133,6 +140,14 @@ export function EditDataSourceView({
|
||||
onTest();
|
||||
};
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const allowedPluginIds = ['grafana-pdc-app', 'grafana-auth-app'];
|
||||
const extensionPointId = PluginExtensionPoints.DataSourceConfig;
|
||||
const { extensions } = getPluginComponentExtensions({ extensionPointId });
|
||||
|
||||
return extensions.filter((e) => allowedPluginIds.includes(e.pluginId));
|
||||
}, []);
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<DataSourceLoadError
|
||||
@@ -190,6 +205,29 @@ export function EditDataSourceView({
|
||||
</DataSourcePluginContextProvider>
|
||||
)}
|
||||
|
||||
{/* Extension point */}
|
||||
{extensions.map((extension) => {
|
||||
const Component = extension.component as React.ComponentType<{
|
||||
context: PluginExtensionDataSourceConfigContext<DataSourceJsonData>;
|
||||
}>;
|
||||
|
||||
return (
|
||||
<div key={extension.id}>
|
||||
<Component
|
||||
context={{
|
||||
dataSource: omit(dataSource, ['secureJsonData']),
|
||||
dataSourceMeta: dataSourceMeta,
|
||||
setJsonData: (jsonData) =>
|
||||
onOptionsChange({
|
||||
...dataSource,
|
||||
jsonData: { ...dataSource.jsonData, ...jsonData },
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<DataSourceTestingStatus testingStatus={testingStatus} exploreUrl={exploreUrl} dataSource={dataSource} />
|
||||
|
||||
<ButtonRow
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Store } from 'redux';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { LayoutModes } from '@grafana/data';
|
||||
import { setAngularLoader, config } from '@grafana/runtime';
|
||||
import { setAngularLoader, config, setPluginExtensionGetter } from '@grafana/runtime';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
@@ -71,6 +71,7 @@ describe('<EditDataSourcePage>', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
api.getDataSourceByIdOrUid = jest.fn().mockResolvedValue(dataSource);
|
||||
setPluginExtensionGetter(jest.fn().mockReturnValue({ extensions: [] }));
|
||||
|
||||
store = configureStore({
|
||||
dataSourceSettings,
|
||||
|
||||
Reference in New Issue
Block a user