瀏覽代碼

Feature: Enable React based options editors for Datasource plugins (#16748)

Dominik Prokop 6 年之前
父節點
當前提交
7aeae84c52

+ 14 - 4
packages/grafana-ui/src/types/datasource.ts

@@ -4,15 +4,24 @@ import { PluginMeta } from './plugin';
 import { TableData, TimeSeries, SeriesData } from './data';
 import { TableData, TimeSeries, SeriesData } from './data';
 import { PanelData } from './panel';
 import { PanelData } from './panel';
 
 
-export class DataSourcePlugin<TQuery extends DataQuery = DataQuery> {
+export interface DataSourcePluginOptionsEditorProps<TOptions> {
+  options: TOptions;
+  onOptionsChange: (options: TOptions) => void;
+}
+export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuery> {
   DataSourceClass: DataSourceConstructor<TQuery>;
   DataSourceClass: DataSourceConstructor<TQuery>;
-  components: DataSourcePluginComponents<TQuery>;
+  components: DataSourcePluginComponents<TOptions, TQuery>;
 
 
   constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
   constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
     this.DataSourceClass = DataSourceClass;
     this.DataSourceClass = DataSourceClass;
     this.components = {};
     this.components = {};
   }
   }
 
 
+  setConfigEditor(editor: React.ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>) {
+    this.components.ConfigEditor = editor;
+    return this;
+  }
+
   setConfigCtrl(ConfigCtrl: any) {
   setConfigCtrl(ConfigCtrl: any) {
     this.components.ConfigCtrl = ConfigCtrl;
     this.components.ConfigCtrl = ConfigCtrl;
     return this;
     return this;
@@ -59,7 +68,7 @@ export class DataSourcePlugin<TQuery extends DataQuery = DataQuery> {
   }
   }
 }
 }
 
 
-export interface DataSourcePluginComponents<TQuery extends DataQuery = DataQuery> {
+export interface DataSourcePluginComponents<TOptions = {}, TQuery extends DataQuery = DataQuery> {
   QueryCtrl?: any;
   QueryCtrl?: any;
   ConfigCtrl?: any;
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
   AnnotationsQueryCtrl?: any;
@@ -67,9 +76,10 @@ export interface DataSourcePluginComponents<TQuery extends DataQuery = DataQuery
   QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
   QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
   ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, TQuery>>;
   ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, TQuery>>;
   ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
   ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
+  ConfigEditor?: React.ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>;
 }
 }
 
 
-interface DataSourceConstructor<TQuery extends DataQuery = DataQuery> {
+export interface DataSourceConstructor<TQuery extends DataQuery = DataQuery> {
   new (instanceSettings: DataSourceInstanceSettings, ...args: any[]): DataSourceApi<TQuery>;
   new (instanceSettings: DataSourceInstanceSettings, ...args: any[]): DataSourceApi<TQuery>;
 }
 }
 
 

+ 9 - 3
public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx

@@ -2,11 +2,13 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage';
 import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage';
 import { NavModel } from 'app/types';
 import { NavModel } from 'app/types';
-import { DataSourceSettings } from '@grafana/ui';
+import { DataSourceSettings, DataSourcePlugin, DataSourceConstructor } from '@grafana/ui';
 import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
 import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
 import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
 import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
 import { setDataSourceName, setIsDefault } from '../state/actions';
 import { setDataSourceName, setIsDefault } from '../state/actions';
 
 
+const pluginMock = new DataSourcePlugin({} as DataSourceConstructor<any>);
+
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
     navModel: {} as NavModel,
     navModel: {} as NavModel,
@@ -18,10 +20,10 @@ const setup = (propOverrides?: object) => {
     setDataSourceName,
     setDataSourceName,
     updateDataSource: jest.fn(),
     updateDataSource: jest.fn(),
     setIsDefault,
     setIsDefault,
+    plugin: pluginMock,
+    ...propOverrides,
   };
   };
 
 
-  Object.assign(props, propOverrides);
-
   return shallow(<DataSourceSettingsPage {...props} />);
   return shallow(<DataSourceSettingsPage {...props} />);
 };
 };
 
 
@@ -35,6 +37,7 @@ describe('Render', () => {
   it('should render loader', () => {
   it('should render loader', () => {
     const wrapper = setup({
     const wrapper = setup({
       dataSource: {} as DataSourceSettings,
       dataSource: {} as DataSourceSettings,
+      plugin: pluginMock,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();
@@ -43,6 +46,7 @@ describe('Render', () => {
   it('should render beta info text', () => {
   it('should render beta info text', () => {
     const wrapper = setup({
     const wrapper = setup({
       dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
       dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
+      plugin: pluginMock,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();
@@ -51,6 +55,7 @@ describe('Render', () => {
   it('should render alpha info text', () => {
   it('should render alpha info text', () => {
     const wrapper = setup({
     const wrapper = setup({
       dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
       dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
+      plugin: pluginMock,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();
@@ -59,6 +64,7 @@ describe('Render', () => {
   it('should render is ready only message', () => {
   it('should render is ready only message', () => {
     const wrapper = setup({
     const wrapper = setup({
       dataSource: { ...getMockDataSource(), readOnly: true },
       dataSource: { ...getMockDataSource(), readOnly: true },
+      plugin: pluginMock,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();

+ 28 - 9
public/app/features/datasources/settings/DataSourceSettingsPage.tsx

@@ -22,9 +22,10 @@ import { getRouteParamsId } from 'app/core/selectors/location';
 
 
 // Types
 // Types
 import { NavModel, Plugin, StoreState } from 'app/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 { getDataSourceLoadingNav } from '../state/navModel';
 import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
 import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
+import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
 
 
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
@@ -36,10 +37,12 @@ export interface Props {
   setDataSourceName: typeof setDataSourceName;
   setDataSourceName: typeof setDataSourceName;
   updateDataSource: typeof updateDataSource;
   updateDataSource: typeof updateDataSource;
   setIsDefault: typeof setIsDefault;
   setIsDefault: typeof setIsDefault;
+  plugin?: DataSourcePlugin;
 }
 }
 
 
 interface State {
 interface State {
   dataSource: DataSourceSettings;
   dataSource: DataSourceSettings;
+  plugin: DataSourcePlugin;
   isTesting?: boolean;
   isTesting?: boolean;
   testingMessage?: string;
   testingMessage?: string;
   testingStatus?: string;
   testingStatus?: string;
@@ -50,14 +53,30 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     super(props);
     super(props);
 
 
     this.state = {
     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() {
   async componentDidMount() {
     const { loadDataSource, pageId } = this.props;
     const { loadDataSource, pageId } = this.props;
-
     await loadDataSource(pageId);
     await loadDataSource(pageId);
+    if (!this.state.plugin) {
+      await this.loadPlugin();
+    }
   }
   }
 
 
   componentDidUpdate(prevProps: Props) {
   componentDidUpdate(prevProps: Props) {
@@ -71,7 +90,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
   onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
   onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
     evt.preventDefault();
     evt.preventDefault();
 
 
-    await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name });
+    await this.props.updateDataSource({ ...this.state.dataSource });
 
 
     this.testDataSource();
     this.testDataSource();
   };
   };
@@ -156,8 +175,8 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
   }
   }
 
 
   render() {
   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 (
     return (
       <Page navModel={navModel}>
       <Page navModel={navModel}>
@@ -175,9 +194,10 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
                   onNameChange={name => setDataSourceName(name)}
                   onNameChange={name => setDataSourceName(name)}
                 />
                 />
 
 
-                {dataSourceMeta.module && (
+                {dataSourceMeta.module && plugin && (
                   <PluginSettings
                   <PluginSettings
-                    dataSource={dataSource}
+                    plugin={plugin}
+                    dataSource={this.state.dataSource}
                     dataSourceMeta={dataSourceMeta}
                     dataSourceMeta={dataSourceMeta}
                     onModelChange={this.onModelChange}
                     onModelChange={this.onModelChange}
                   />
                   />
@@ -218,7 +238,6 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
 function mapStateToProps(state: StoreState) {
 function mapStateToProps(state: StoreState) {
   const pageId = getRouteParamsId(state.location);
   const pageId = getRouteParamsId(state.location);
   const dataSource = getDataSource(state.dataSources, pageId);
   const dataSource = getDataSource(state.dataSources, pageId);
-
   return {
   return {
     navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')),
     navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')),
     dataSource: getDataSource(state.dataSources, pageId),
     dataSource: getDataSource(state.dataSources, pageId),

+ 29 - 6
public/app/features/datasources/settings/PluginSettings.tsx

@@ -1,10 +1,11 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 import { Plugin } from 'app/types';
 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';
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 
 
 export interface Props {
 export interface Props {
+  plugin: DataSourcePlugin;
   dataSource: DataSourceSettings;
   dataSource: DataSourceSettings;
   dataSourceMeta: Plugin;
   dataSourceMeta: Plugin;
   onModelChange: (dataSource: DataSourceSettings) => void;
   onModelChange: (dataSource: DataSourceSettings) => void;
@@ -25,21 +26,29 @@ export class PluginSettings extends PureComponent<Props> {
       ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) },
       ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) },
       onModelChanged: this.onModelChanged,
       onModelChanged: this.onModelChanged,
     };
     };
+    this.onModelChanged = this.onModelChanged.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
+    const { plugin } = this.props;
+
     if (!this.element) {
     if (!this.element) {
       return;
       return;
     }
     }
 
 
-    const loader = getAngularLoader();
-    const template = '<plugin-component type="datasource-config-ctrl" />';
+    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 = '<plugin-component type="datasource-config-ctrl" />';
 
 
-    this.component = loader.load(this.element, this.scopeProps, template);
+      this.component = loader.load(this.element, this.scopeProps, template);
+    }
   }
   }
 
 
   componentDidUpdate(prevProps) {
   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.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource);
 
 
       this.component.digest();
       this.component.digest();
@@ -57,7 +66,21 @@ export class PluginSettings extends PureComponent<Props> {
   };
   };
 
 
   render() {
   render() {
-    return <div ref={element => (this.element = element)} />;
+    const { plugin, dataSource } = this.props;
+
+    if (!plugin) {
+      return null;
+    }
+
+    return (
+      <div ref={element => (this.element = element)}>
+        {plugin.components.ConfigEditor &&
+          React.createElement(plugin.components.ConfigEditor, {
+            options: dataSource,
+            onOptionsChange: this.onModelChanged,
+          })}
+      </div>
+    );
   }
   }
 }
 }
 
 

+ 24 - 0
public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap

@@ -85,6 +85,12 @@ exports[`Render should render alpha info text 1`] = `
             }
             }
           }
           }
           onModelChange={[Function]}
           onModelChange={[Function]}
+          plugin={
+            DataSourcePlugin {
+              "DataSourceClass": Object {},
+              "components": Object {},
+            }
+          }
         />
         />
         <div
         <div
           className="gf-form-group"
           className="gf-form-group"
@@ -186,6 +192,12 @@ exports[`Render should render beta info text 1`] = `
             }
             }
           }
           }
           onModelChange={[Function]}
           onModelChange={[Function]}
+          plugin={
+            DataSourcePlugin {
+              "DataSourceClass": Object {},
+              "components": Object {},
+            }
+          }
         />
         />
         <div
         <div
           className="gf-form-group"
           className="gf-form-group"
@@ -284,6 +296,12 @@ exports[`Render should render component 1`] = `
             }
             }
           }
           }
           onModelChange={[Function]}
           onModelChange={[Function]}
+          plugin={
+            DataSourcePlugin {
+              "DataSourceClass": Object {},
+              "components": Object {},
+            }
+          }
         />
         />
         <div
         <div
           className="gf-form-group"
           className="gf-form-group"
@@ -387,6 +405,12 @@ exports[`Render should render is ready only message 1`] = `
             }
             }
           }
           }
           onModelChange={[Function]}
           onModelChange={[Function]}
+          plugin={
+            DataSourcePlugin {
+              "DataSourceClass": Object {},
+              "components": Object {},
+            }
+          }
         />
         />
         <div
         <div
           className="gf-form-group"
           className="gf-form-group"

+ 2 - 2
public/app/features/plugins/plugin_loader.ts

@@ -160,10 +160,10 @@ export function importPluginModule(path: string): Promise<any> {
   return System.import(path);
   return System.import(path);
 }
 }
 
 
-export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin> {
+export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<any>> {
   return importPluginModule(path).then(pluginExports => {
   return importPluginModule(path).then(pluginExports => {
     if (pluginExports.plugin) {
     if (pluginExports.plugin) {
-      return pluginExports.plugin as DataSourcePlugin;
+      return pluginExports.plugin as DataSourcePlugin<any>;
     }
     }
 
 
     if (pluginExports.Datasource) {
     if (pluginExports.Datasource) {

+ 0 - 0
public/app/plugins/datasource/testdata/module.ts → public/app/plugins/datasource/testdata/module.tsx