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 { 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),
|
||||
|
@ -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<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) {
|
||||
|
Loading…
Reference in New Issue
Block a user