Commit 7aeae84c by Dominik Prokop Committed by GitHub

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

parent 2d6b33ab
......@@ -4,15 +4,24 @@ import { PluginMeta } from './plugin';
import { TableData, TimeSeries, SeriesData } from './data';
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>;
components: DataSourcePluginComponents<TQuery>;
components: DataSourcePluginComponents<TOptions, TQuery>;
constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
this.DataSourceClass = DataSourceClass;
this.components = {};
}
setConfigEditor(editor: React.ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>) {
this.components.ConfigEditor = editor;
return this;
}
setConfigCtrl(ConfigCtrl: any) {
this.components.ConfigCtrl = ConfigCtrl;
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;
ConfigCtrl?: any;
AnnotationsQueryCtrl?: any;
......@@ -67,9 +76,10 @@ export interface DataSourcePluginComponents<TQuery extends DataQuery = DataQuery
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, TQuery>>;
ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, TQuery>>;
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>;
}
......
......@@ -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<any>);
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(<DataSourceSettingsPage {...props} />);
};
......@@ -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();
......
......@@ -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<Props, State> {
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<Props, State> {
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
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<Props, State> {
}
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 (
<Page navModel={navModel}>
......@@ -175,9 +194,10 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
onNameChange={name => setDataSourceName(name)}
/>
{dataSourceMeta.module && (
{dataSourceMeta.module && plugin && (
<PluginSettings
dataSource={dataSource}
plugin={plugin}
dataSource={this.state.dataSource}
dataSourceMeta={dataSourceMeta}
onModelChange={this.onModelChange}
/>
......@@ -218,7 +238,6 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
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),
......
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<Props> {
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 = '<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) {
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<Props> {
};
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>
);
}
}
......
......@@ -85,6 +85,12 @@ exports[`Render should render alpha info text 1`] = `
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
......@@ -186,6 +192,12 @@ exports[`Render should render beta info text 1`] = `
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
......@@ -284,6 +296,12 @@ exports[`Render should render component 1`] = `
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
......@@ -387,6 +405,12 @@ exports[`Render should render is ready only message 1`] = `
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
......
......@@ -160,10 +160,10 @@ export function importPluginModule(path: string): Promise<any> {
return System.import(path);
}
export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin> {
export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<any>> {
return importPluginModule(path).then(pluginExports => {
if (pluginExports.plugin) {
return pluginExports.plugin as DataSourcePlugin;
return pluginExports.plugin as DataSourcePlugin<any>;
}
if (pluginExports.Datasource) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment