From 7aeae84c523fda5f694758d2eb6aac6053809c81 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 24 Apr 2019 23:18:51 +0200 Subject: [PATCH] Feature: Enable React based options editors for Datasource plugins (#16748) --- packages/grafana-ui/src/types/datasource.ts | 18 +++++++-- .../settings/DataSourceSettingsPage.test.tsx | 12 ++++-- .../settings/DataSourceSettingsPage.tsx | 37 ++++++++++++++----- .../datasources/settings/PluginSettings.tsx | 35 +++++++++++++++--- .../DataSourceSettingsPage.test.tsx.snap | 24 ++++++++++++ public/app/features/plugins/plugin_loader.ts | 4 +- .../testdata/{module.ts => module.tsx} | 0 7 files changed, 106 insertions(+), 24 deletions(-) rename public/app/plugins/datasource/testdata/{module.ts => module.tsx} (100%) diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 7dfa7bcea9c..13e7db09dd3 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -4,15 +4,24 @@ import { PluginMeta } from './plugin'; import { TableData, TimeSeries, SeriesData } from './data'; import { PanelData } from './panel'; -export class DataSourcePlugin { +export interface DataSourcePluginOptionsEditorProps { + options: TOptions; + onOptionsChange: (options: TOptions) => void; +} +export class DataSourcePlugin { DataSourceClass: DataSourceConstructor; - components: DataSourcePluginComponents; + components: DataSourcePluginComponents; constructor(DataSourceClass: DataSourceConstructor) { this.DataSourceClass = DataSourceClass; this.components = {}; } + setConfigEditor(editor: React.ComponentType>) { + this.components.ConfigEditor = editor; + return this; + } + setConfigCtrl(ConfigCtrl: any) { this.components.ConfigCtrl = ConfigCtrl; return this; @@ -59,7 +68,7 @@ export class DataSourcePlugin { } } -export interface DataSourcePluginComponents { +export interface DataSourcePluginComponents { QueryCtrl?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; @@ -67,9 +76,10 @@ export interface DataSourcePluginComponents>; ExploreQueryField?: ComponentClass>; ExploreStartPage?: ComponentClass; + ConfigEditor?: React.ComponentType>; } -interface DataSourceConstructor { +export interface DataSourceConstructor { new (instanceSettings: DataSourceInstanceSettings, ...args: any[]): DataSourceApi; } diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx index 204eeb8b1e9..d4664418f07 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx @@ -2,11 +2,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage'; import { NavModel } from 'app/types'; -import { DataSourceSettings } from '@grafana/ui'; +import { DataSourceSettings, DataSourcePlugin, DataSourceConstructor } from '@grafana/ui'; import { getMockDataSource } from '../__mocks__/dataSourcesMocks'; import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks'; import { setDataSourceName, setIsDefault } from '../state/actions'; +const pluginMock = new DataSourcePlugin({} as DataSourceConstructor); + const setup = (propOverrides?: object) => { const props: Props = { navModel: {} as NavModel, @@ -18,10 +20,10 @@ const setup = (propOverrides?: object) => { setDataSourceName, updateDataSource: jest.fn(), setIsDefault, + plugin: pluginMock, + ...propOverrides, }; - Object.assign(props, propOverrides); - return shallow(); }; @@ -35,6 +37,7 @@ describe('Render', () => { it('should render loader', () => { const wrapper = setup({ dataSource: {} as DataSourceSettings, + plugin: pluginMock, }); expect(wrapper).toMatchSnapshot(); @@ -43,6 +46,7 @@ describe('Render', () => { it('should render beta info text', () => { const wrapper = setup({ dataSourceMeta: { ...getMockPlugin(), state: 'beta' }, + plugin: pluginMock, }); expect(wrapper).toMatchSnapshot(); @@ -51,6 +55,7 @@ describe('Render', () => { it('should render alpha info text', () => { const wrapper = setup({ dataSourceMeta: { ...getMockPlugin(), state: 'alpha' }, + plugin: pluginMock, }); expect(wrapper).toMatchSnapshot(); @@ -59,6 +64,7 @@ describe('Render', () => { it('should render is ready only message', () => { const wrapper = setup({ dataSource: { ...getMockDataSource(), readOnly: true }, + plugin: pluginMock, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 1bb94e7bb6d..489ccd4234b 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -22,9 +22,10 @@ import { getRouteParamsId } from 'app/core/selectors/location'; // Types import { NavModel, Plugin, StoreState } from 'app/types/'; -import { DataSourceSettings } from '@grafana/ui/src/types/'; +import { DataSourceSettings, DataSourcePlugin } from '@grafana/ui/src/types/'; import { getDataSourceLoadingNav } from '../state/navModel'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; +import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; export interface Props { navModel: NavModel; @@ -36,10 +37,12 @@ export interface Props { setDataSourceName: typeof setDataSourceName; updateDataSource: typeof updateDataSource; setIsDefault: typeof setIsDefault; + plugin?: DataSourcePlugin; } interface State { dataSource: DataSourceSettings; + plugin: DataSourcePlugin; isTesting?: boolean; testingMessage?: string; testingStatus?: string; @@ -50,14 +53,30 @@ export class DataSourceSettingsPage extends PureComponent { super(props); this.state = { - dataSource: {} as DataSourceSettings, + dataSource: props.dataSource, + plugin: props.plugin, }; } + async loadPlugin(pluginId?: string) { + const { dataSourceMeta } = this.props; + let importedPlugin: DataSourcePlugin; + + try { + importedPlugin = await importDataSourcePlugin(dataSourceMeta.module); + } catch (e) { + console.log('Failed to import plugin module', e); + } + + this.setState({ plugin: importedPlugin }); + } + async componentDidMount() { const { loadDataSource, pageId } = this.props; - await loadDataSource(pageId); + if (!this.state.plugin) { + await this.loadPlugin(); + } } componentDidUpdate(prevProps: Props) { @@ -71,7 +90,7 @@ export class DataSourceSettingsPage extends PureComponent { onSubmit = async (evt: React.FormEvent) => { evt.preventDefault(); - await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name }); + await this.props.updateDataSource({ ...this.state.dataSource }); this.testDataSource(); }; @@ -156,8 +175,8 @@ export class DataSourceSettingsPage extends PureComponent { } render() { - const { dataSource, dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props; - const { testingMessage, testingStatus } = this.state; + const { dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props; + const { testingMessage, testingStatus, plugin, dataSource } = this.state; return ( @@ -175,9 +194,10 @@ export class DataSourceSettingsPage extends PureComponent { onNameChange={name => setDataSourceName(name)} /> - {dataSourceMeta.module && ( + {dataSourceMeta.module && plugin && ( @@ -218,7 +238,6 @@ export class DataSourceSettingsPage extends PureComponent { function mapStateToProps(state: StoreState) { const pageId = getRouteParamsId(state.location); const dataSource = getDataSource(state.dataSources, pageId); - return { navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')), dataSource: getDataSource(state.dataSources, pageId), diff --git a/public/app/features/datasources/settings/PluginSettings.tsx b/public/app/features/datasources/settings/PluginSettings.tsx index 8b65accd50a..5761af82710 100644 --- a/public/app/features/datasources/settings/PluginSettings.tsx +++ b/public/app/features/datasources/settings/PluginSettings.tsx @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; import { Plugin } from 'app/types'; -import { DataSourceSettings } from '@grafana/ui/src/types'; +import { DataSourceSettings, DataSourcePlugin } from '@grafana/ui/src/types'; import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; export interface Props { + plugin: DataSourcePlugin; dataSource: DataSourceSettings; dataSourceMeta: Plugin; onModelChange: (dataSource: DataSourceSettings) => void; @@ -25,21 +26,29 @@ export class PluginSettings extends PureComponent { ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) }, onModelChanged: this.onModelChanged, }; + this.onModelChanged = this.onModelChanged.bind(this); } componentDidMount() { + const { plugin } = this.props; + if (!this.element) { return; } - const loader = getAngularLoader(); - const template = ''; + if (!plugin.components.ConfigEditor) { + // React editor is not specified, let's render angular editor + // How to apprach this better? Introduce ReactDataSourcePlugin interface and typeguard it here? + const loader = getAngularLoader(); + const template = ''; - this.component = loader.load(this.element, this.scopeProps, template); + this.component = loader.load(this.element, this.scopeProps, template); + } } componentDidUpdate(prevProps) { - if (this.props.dataSource !== prevProps.dataSource) { + const { plugin } = this.props; + if (!plugin.components.ConfigEditor && this.props.dataSource !== prevProps.dataSource) { this.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource); this.component.digest(); @@ -57,7 +66,21 @@ export class PluginSettings extends PureComponent { }; render() { - return
(this.element = element)} />; + const { plugin, dataSource } = this.props; + + if (!plugin) { + return null; + } + + return ( +
(this.element = element)}> + {plugin.components.ConfigEditor && + React.createElement(plugin.components.ConfigEditor, { + options: dataSource, + onOptionsChange: this.onModelChanged, + })} +
+ ); } } diff --git a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap index c9bcf8d2210..13495a99bf5 100644 --- a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap @@ -85,6 +85,12 @@ exports[`Render should render alpha info text 1`] = ` } } onModelChange={[Function]} + plugin={ + DataSourcePlugin { + "DataSourceClass": Object {}, + "components": Object {}, + } + } />
{ return System.import(path); } -export function importDataSourcePlugin(path: string): Promise { +export function importDataSourcePlugin(path: string): Promise> { return importPluginModule(path).then(pluginExports => { if (pluginExports.plugin) { - return pluginExports.plugin as DataSourcePlugin; + return pluginExports.plugin as DataSourcePlugin; } if (pluginExports.Datasource) { diff --git a/public/app/plugins/datasource/testdata/module.ts b/public/app/plugins/datasource/testdata/module.tsx similarity index 100% rename from public/app/plugins/datasource/testdata/module.ts rename to public/app/plugins/datasource/testdata/module.tsx