mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Feature: Enable React based options editors for Datasource plugins (#16748)
This commit is contained in:
parent
2d6b33ab61
commit
7aeae84c52
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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 { dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props;
|
||||||
const { testingMessage, testingStatus } = this.state;
|
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),
|
||||||
|
@ -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();
|
if (!plugin.components.ConfigEditor) {
|
||||||
const template = '<plugin-component type="datasource-config-ctrl" />';
|
// 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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user