mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Make importDataSourcePlugin cancelable (#21430)
* make importDataSourcePlugin cancelable * fix imported plugin assignment * init datasource plugin to redux * remove commented * testDataSource to redux * add err console log * isTesting is never used * tests, loadError type * more tests, testingStatus obj
This commit is contained in:
@@ -24,6 +24,8 @@ const setup = (propOverrides?: object) => {
|
|||||||
loadDataSource: jest.fn(),
|
loadDataSource: jest.fn(),
|
||||||
setDataSourceName,
|
setDataSourceName,
|
||||||
updateDataSource: jest.fn(),
|
updateDataSource: jest.fn(),
|
||||||
|
initDataSourceSettings: jest.fn(),
|
||||||
|
testDataSource: jest.fn(),
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
dataSourceLoaded,
|
dataSourceLoaded,
|
||||||
query: {},
|
query: {},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import isString from 'lodash/isString';
|
import isString from 'lodash/isString';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
// Components
|
// Components
|
||||||
@@ -11,11 +10,15 @@ import BasicSettings from './BasicSettings';
|
|||||||
import ButtonRow from './ButtonRow';
|
import ButtonRow from './ButtonRow';
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|
||||||
// Actions & selectors
|
// Actions & selectors
|
||||||
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
||||||
import { deleteDataSource, loadDataSource, updateDataSource } from '../state/actions';
|
import {
|
||||||
|
deleteDataSource,
|
||||||
|
loadDataSource,
|
||||||
|
updateDataSource,
|
||||||
|
initDataSourceSettings,
|
||||||
|
testDataSource,
|
||||||
|
} from '../state/actions';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||||
// Types
|
// Types
|
||||||
@@ -24,8 +27,8 @@ import { UrlQueryMap } from '@grafana/runtime';
|
|||||||
import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/data';
|
||||||
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';
|
|
||||||
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||||
|
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@@ -38,55 +41,22 @@ export interface Props {
|
|||||||
updateDataSource: typeof updateDataSource;
|
updateDataSource: typeof updateDataSource;
|
||||||
setIsDefault: typeof setIsDefault;
|
setIsDefault: typeof setIsDefault;
|
||||||
dataSourceLoaded: typeof dataSourceLoaded;
|
dataSourceLoaded: typeof dataSourceLoaded;
|
||||||
|
initDataSourceSettings: typeof initDataSourceSettings;
|
||||||
|
testDataSource: typeof testDataSource;
|
||||||
plugin?: GenericDataSourcePlugin;
|
plugin?: GenericDataSourcePlugin;
|
||||||
query: UrlQueryMap;
|
query: UrlQueryMap;
|
||||||
page?: string;
|
page?: string;
|
||||||
|
testingStatus?: {
|
||||||
|
message?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
loadError?: Error | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export class DataSourceSettingsPage extends PureComponent<Props> {
|
||||||
plugin?: GenericDataSourcePlugin;
|
componentDidMount() {
|
||||||
isTesting?: boolean;
|
const { initDataSourceSettings, pageId } = this.props;
|
||||||
testingMessage?: string;
|
initDataSourceSettings(pageId);
|
||||||
testingStatus?: string;
|
|
||||||
loadError?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
plugin: props.plugin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadPlugin(pluginId?: string) {
|
|
||||||
const { dataSourceMeta } = this.props;
|
|
||||||
let importedPlugin: GenericDataSourcePlugin;
|
|
||||||
|
|
||||||
try {
|
|
||||||
importedPlugin = await importDataSourcePlugin(dataSourceMeta);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to import plugin module', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ plugin: importedPlugin });
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const { loadDataSource, pageId } = this.props;
|
|
||||||
if (isNaN(pageId)) {
|
|
||||||
this.setState({ loadError: 'Invalid ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await loadDataSource(pageId);
|
|
||||||
if (!this.state.plugin) {
|
|
||||||
await this.loadPlugin();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ loadError: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -136,40 +106,9 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async testDataSource() {
|
testDataSource() {
|
||||||
const dsApi = await getDatasourceSrv().get(this.props.dataSource.name);
|
const { dataSource, testDataSource } = this.props;
|
||||||
|
testDataSource(dataSource.name);
|
||||||
if (!dsApi.testDatasource) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
|
|
||||||
|
|
||||||
backendSrv.withNoBackendCache(async () => {
|
|
||||||
try {
|
|
||||||
const result = await dsApi.testDatasource();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isTesting: false,
|
|
||||||
testingStatus: result.status,
|
|
||||||
testingMessage: result.message,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
let message = '';
|
|
||||||
|
|
||||||
if (err.statusText) {
|
|
||||||
message = 'HTTP Error ' + err.statusText;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isTesting: false,
|
|
||||||
testingStatus: 'error',
|
|
||||||
testingMessage: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasDataSource() {
|
get hasDataSource() {
|
||||||
@@ -218,7 +157,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderConfigPageBody(page: string) {
|
renderConfigPageBody(page: string) {
|
||||||
const { plugin } = this.state;
|
const { plugin } = this.props;
|
||||||
if (!plugin || !plugin.configPages) {
|
if (!plugin || !plugin.configPages) {
|
||||||
return null; // still loading
|
return null; // still loading
|
||||||
}
|
}
|
||||||
@@ -233,8 +172,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSettings() {
|
renderSettings() {
|
||||||
const { dataSourceMeta, setDataSourceName, setIsDefault, dataSource } = this.props;
|
const { dataSourceMeta, setDataSourceName, setIsDefault, dataSource, testingStatus, plugin } = this.props;
|
||||||
const { testingMessage, testingStatus, plugin } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
@@ -265,10 +203,10 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="gf-form-group">
|
<div className="gf-form-group">
|
||||||
{testingMessage && (
|
{testingStatus && testingStatus.message && (
|
||||||
<div className={`alert-${testingStatus} alert`} aria-label={e2e.pages.DataSource.selectors.alert}>
|
<div className={`alert-${testingStatus.status} alert`} aria-label={e2e.pages.DataSource.selectors.alert}>
|
||||||
<div className="alert-icon">
|
<div className="alert-icon">
|
||||||
{testingStatus === 'error' ? (
|
{testingStatus.status === 'error' ? (
|
||||||
<i className="fa fa-exclamation-triangle" />
|
<i className="fa fa-exclamation-triangle" />
|
||||||
) : (
|
) : (
|
||||||
<i className="fa fa-check" />
|
<i className="fa fa-check" />
|
||||||
@@ -276,7 +214,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="alert-body">
|
<div className="alert-body">
|
||||||
<div className="alert-title" aria-label={e2e.pages.DataSource.selectors.alertMessage}>
|
<div className="alert-title" aria-label={e2e.pages.DataSource.selectors.alertMessage}>
|
||||||
{testingMessage}
|
{testingStatus.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,8 +232,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { navModel, page } = this.props;
|
const { navModel, page, loadError } = this.props;
|
||||||
const { loadError } = this.state;
|
|
||||||
|
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
return this.renderLoadError(loadError);
|
return this.renderLoadError(loadError);
|
||||||
@@ -315,6 +252,7 @@ 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);
|
||||||
const page = state.location.query.page as string;
|
const page = state.location.query.page as string;
|
||||||
|
const { plugin, loadError, testingStatus } = state.dataSourceSettings;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(
|
navModel: getNavModel(
|
||||||
@@ -327,6 +265,9 @@ function mapStateToProps(state: StoreState) {
|
|||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
query: state.location.query,
|
query: state.location.query,
|
||||||
page,
|
page,
|
||||||
|
plugin,
|
||||||
|
loadError,
|
||||||
|
testingStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +278,10 @@ const mapDispatchToProps = {
|
|||||||
updateDataSource,
|
updateDataSource,
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
dataSourceLoaded,
|
dataSourceLoaded,
|
||||||
|
initDataSourceSettings,
|
||||||
|
testDataSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettingsPage));
|
export default hot(module)(
|
||||||
|
connectWithCleanUp(mapStateToProps, mapDispatchToProps, state => state.dataSourceSettings)(DataSourceSettingsPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { findNewName, nameExits } from './actions';
|
import { findNewName, nameExits, InitDataSourceSettingDependencies, testDataSource } from './actions';
|
||||||
import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
|
import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
|
||||||
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
|
import {
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
|
testDataSourceStarting,
|
||||||
|
testDataSourceSucceeded,
|
||||||
|
testDataSourceFailed,
|
||||||
|
} from './reducers';
|
||||||
|
import { initDataSourceSettings } from '../state/actions';
|
||||||
|
import { ThunkResult, ThunkDispatch } from 'app/types';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
import * as DatasourceSrv from 'app/features/plugins/datasource_srv';
|
||||||
|
|
||||||
|
jest.mock('app/features/plugins/datasource_srv');
|
||||||
|
const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock<DatasourceSrv.DatasourceSrv>;
|
||||||
|
|
||||||
describe('Name exists', () => {
|
describe('Name exists', () => {
|
||||||
const plugins = getMockPlugins(5);
|
const plugins = getMockPlugins(5);
|
||||||
@@ -42,3 +57,129 @@ describe('Find new name', () => {
|
|||||||
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
|
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initDataSourceSettings', () => {
|
||||||
|
describe('when pageId is not a number', () => {
|
||||||
|
it('then initDataSourceSettingsFailed should be dispatched', async () => {
|
||||||
|
const dispatchedActions = await thunkTester({})
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched('some page');
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Invalid ID'))]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when pageId is a number', () => {
|
||||||
|
it('then initDataSourceSettingsSucceeded should be dispatched', async () => {
|
||||||
|
const thunkMock = (): ThunkResult<void> => (dispatch: ThunkDispatch, getState) => {};
|
||||||
|
const dataSource = { type: 'app' };
|
||||||
|
const dataSourceMeta = { id: 'some id' };
|
||||||
|
const dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource: jest.fn(thunkMock),
|
||||||
|
getDataSource: jest.fn().mockReturnValue(dataSource),
|
||||||
|
getDataSourceMeta: jest.fn().mockReturnValue(dataSourceMeta),
|
||||||
|
importDataSourcePlugin: jest.fn().mockReturnValue({} as GenericDataSourcePlugin),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
dataSourceSettings: {},
|
||||||
|
dataSources: {},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched(256, dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsSucceeded({} as GenericDataSourcePlugin)]);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledWith(256);
|
||||||
|
|
||||||
|
expect(dependencies.getDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.getDataSource).toHaveBeenCalledWith({}, 256);
|
||||||
|
|
||||||
|
expect(dependencies.getDataSourceMeta).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.getDataSourceMeta).toHaveBeenCalledWith({}, 'app');
|
||||||
|
|
||||||
|
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledWith(dataSourceMeta);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when plugin loading fails', () => {
|
||||||
|
it('then initDataSourceSettingsFailed should be dispatched', async () => {
|
||||||
|
const dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource: jest.fn().mockImplementation(() => {
|
||||||
|
throw new Error('Error loading plugin');
|
||||||
|
}),
|
||||||
|
getDataSource: jest.fn(),
|
||||||
|
getDataSourceMeta: jest.fn(),
|
||||||
|
importDataSourcePlugin: jest.fn(),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
dataSourceSettings: {},
|
||||||
|
dataSources: {},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched(301, dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Error loading plugin'))]);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledWith(301);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('testDataSource', () => {
|
||||||
|
describe('when a datasource is tested', () => {
|
||||||
|
it('then testDataSourceStarting and testDataSourceSucceeded should be dispatched', async () => {
|
||||||
|
getDatasourceSrvMock.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
testDatasource: jest.fn().mockReturnValue({
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
const state = {
|
||||||
|
testingStatus: {
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(testDataSource)
|
||||||
|
.whenThunkIsDispatched('Azure Monitor');
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceSucceeded(state.testingStatus)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then testDataSourceFailed should be dispatched', async () => {
|
||||||
|
getDatasourceSrvMock.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
testDatasource: jest.fn().mockImplementation(() => {
|
||||||
|
throw new Error('Error testing datasource');
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
const result = {
|
||||||
|
message: 'Error testing datasource',
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
testingStatus: {
|
||||||
|
message: '',
|
||||||
|
status: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(testDataSource)
|
||||||
|
.whenThunkIsDispatched('Azure Monitor');
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import config from '../../../core/config';
|
import config from '../../../core/config';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||||
import { DataSourcePluginCategory, ThunkResult } from 'app/types';
|
import { DataSourcePluginCategory, ThunkResult, ThunkDispatch } from 'app/types';
|
||||||
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
||||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||||
import {
|
import {
|
||||||
@@ -13,14 +13,90 @@ import {
|
|||||||
dataSourcePluginsLoad,
|
dataSourcePluginsLoad,
|
||||||
dataSourcePluginsLoaded,
|
dataSourcePluginsLoaded,
|
||||||
dataSourcesLoaded,
|
dataSourcesLoaded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
testDataSourceStarting,
|
||||||
|
testDataSourceSucceeded,
|
||||||
|
testDataSourceFailed,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { buildCategories } from './buildCategories';
|
import { buildCategories } from './buildCategories';
|
||||||
|
import { getDataSource, getDataSourceMeta } from './selectors';
|
||||||
|
|
||||||
export interface DataSourceTypesLoadedPayload {
|
export interface DataSourceTypesLoadedPayload {
|
||||||
plugins: DataSourcePluginMeta[];
|
plugins: DataSourcePluginMeta[];
|
||||||
categories: DataSourcePluginCategory[];
|
categories: DataSourcePluginCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InitDataSourceSettingDependencies {
|
||||||
|
loadDataSource: typeof loadDataSource;
|
||||||
|
getDataSource: typeof getDataSource;
|
||||||
|
getDataSourceMeta: typeof getDataSourceMeta;
|
||||||
|
importDataSourcePlugin: typeof importDataSourcePlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initDataSourceSettings = (
|
||||||
|
pageId: number,
|
||||||
|
dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource,
|
||||||
|
getDataSource,
|
||||||
|
getDataSourceMeta,
|
||||||
|
importDataSourcePlugin,
|
||||||
|
}
|
||||||
|
): ThunkResult<void> => {
|
||||||
|
return async (dispatch: ThunkDispatch, getState) => {
|
||||||
|
if (isNaN(pageId)) {
|
||||||
|
dispatch(initDataSourceSettingsFailed(new Error('Invalid ID')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(dependencies.loadDataSource(pageId));
|
||||||
|
if (getState().dataSourceSettings.plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = dependencies.getDataSource(getState().dataSources, pageId);
|
||||||
|
const dataSourceMeta = dependencies.getDataSourceMeta(getState().dataSources, dataSource.type);
|
||||||
|
const importedPlugin = await dependencies.importDataSourcePlugin(dataSourceMeta);
|
||||||
|
|
||||||
|
dispatch(initDataSourceSettingsSucceeded(importedPlugin));
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to import plugin module', err);
|
||||||
|
dispatch(initDataSourceSettingsFailed(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testDataSource = (dataSourceName: string): ThunkResult<void> => {
|
||||||
|
return async (dispatch: ThunkDispatch, getState) => {
|
||||||
|
const dsApi = await getDatasourceSrv().get(dataSourceName);
|
||||||
|
|
||||||
|
if (!dsApi.testDatasource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(testDataSourceStarting());
|
||||||
|
|
||||||
|
getBackendSrv().withNoBackendCache(async () => {
|
||||||
|
try {
|
||||||
|
const result = await dsApi.testDatasource();
|
||||||
|
|
||||||
|
dispatch(testDataSourceSucceeded(result));
|
||||||
|
} catch (err) {
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
if (err.statusText) {
|
||||||
|
message = 'HTTP Error ' + err.statusText;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(testDataSourceFailed({ message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function loadDataSources(): ThunkResult<void> {
|
export function loadDataSources(): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const response = await getBackendSrv().get('/api/datasources');
|
const response = await getBackendSrv().get('/api/datasources');
|
||||||
@@ -123,7 +199,7 @@ export function findNewName(dataSources: ItemWithName[], name: string) {
|
|||||||
function updateFrontendSettings() {
|
function updateFrontendSettings() {
|
||||||
return getBackendSrv()
|
return getBackendSrv()
|
||||||
.get('/api/frontend/settings')
|
.get('/api/frontend/settings')
|
||||||
.then(settings => {
|
.then((settings: any) => {
|
||||||
config.datasources = settings.datasources;
|
config.datasources = settings.datasources;
|
||||||
config.defaultDatasource = settings.defaultDatasource;
|
config.defaultDatasource = settings.defaultDatasource;
|
||||||
getDatasourceSrv().init();
|
getDatasourceSrv().init();
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import {
|
|||||||
setDataSourcesSearchQuery,
|
setDataSourcesSearchQuery,
|
||||||
setDataSourceTypeSearchQuery,
|
setDataSourceTypeSearchQuery,
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
|
dataSourceSettingsReducer,
|
||||||
|
initialDataSourceSettingsState,
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { getMockDataSource, getMockDataSources } from '../__mocks__/dataSourcesMocks';
|
import { getMockDataSource, getMockDataSources } from '../__mocks__/dataSourcesMocks';
|
||||||
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
import { DataSourcesState } from 'app/types';
|
import { DataSourcesState, DataSourceSettingsState } from 'app/types';
|
||||||
import { PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
import { PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
|
||||||
const mockPlugin = () =>
|
const mockPlugin = () =>
|
||||||
({
|
({
|
||||||
@@ -136,3 +141,34 @@ describe('dataSourcesReducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dataSourceSettingsReducer', () => {
|
||||||
|
describe('when initDataSourceSettingsSucceeded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester<DataSourceSettingsState>()
|
||||||
|
.givenReducer(dataSourceSettingsReducer, { ...initialDataSourceSettingsState })
|
||||||
|
.whenActionIsDispatched(initDataSourceSettingsSucceeded({} as GenericDataSourcePlugin))
|
||||||
|
.thenStateShouldEqual({
|
||||||
|
...initialDataSourceSettingsState,
|
||||||
|
plugin: {} as GenericDataSourcePlugin,
|
||||||
|
loadError: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when initDataSourceSettingsFailed is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester<DataSourceSettingsState>()
|
||||||
|
.givenReducer(dataSourceSettingsReducer, {
|
||||||
|
...initialDataSourceSettingsState,
|
||||||
|
plugin: {} as GenericDataSourcePlugin,
|
||||||
|
})
|
||||||
|
.whenActionIsDispatched(initDataSourceSettingsFailed(new Error('Some error')))
|
||||||
|
.thenStatePredicateShouldEqual(resultingState => {
|
||||||
|
expect(resultingState.plugin).toEqual(null);
|
||||||
|
expect(resultingState.loadError).toEqual('Some error');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
||||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||||
|
|
||||||
import { DataSourcesState } from 'app/types';
|
import { DataSourcesState, DataSourceSettingsState } from 'app/types';
|
||||||
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
import { DataSourceTypesLoadedPayload } from './actions';
|
import { DataSourceTypesLoadedPayload } from './actions';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
|
||||||
export const initialState: DataSourcesState = {
|
export const initialState: DataSourcesState = {
|
||||||
dataSources: [],
|
dataSources: [],
|
||||||
@@ -94,6 +95,76 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialDataSourceSettingsState: DataSourceSettingsState = {
|
||||||
|
testingStatus: {
|
||||||
|
status: null,
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
loadError: null,
|
||||||
|
plugin: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initDataSourceSettingsSucceeded = createAction<GenericDataSourcePlugin>(
|
||||||
|
'dataSourceSettings/initDataSourceSettingsSucceeded'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const initDataSourceSettingsFailed = createAction<Error>('dataSourceSettings/initDataSourceSettingsFailed');
|
||||||
|
|
||||||
|
export const testDataSourceStarting = createAction<undefined>('dataSourceSettings/testDataSourceStarting');
|
||||||
|
|
||||||
|
export const testDataSourceSucceeded = createAction<{
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
}>('dataSourceSettings/testDataSourceSucceeded');
|
||||||
|
|
||||||
|
export const testDataSourceFailed = createAction<{ message: string }>('dataSourceSettings/testDataSourceFailed');
|
||||||
|
|
||||||
|
export const dataSourceSettingsReducer = (
|
||||||
|
state: DataSourceSettingsState = initialDataSourceSettingsState,
|
||||||
|
action: AnyAction
|
||||||
|
): DataSourceSettingsState => {
|
||||||
|
if (initDataSourceSettingsSucceeded.match(action)) {
|
||||||
|
return { ...state, plugin: action.payload, loadError: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initDataSourceSettingsFailed.match(action)) {
|
||||||
|
return { ...state, plugin: null, loadError: action.payload.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceStarting.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
message: 'Testing...',
|
||||||
|
status: 'info',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceSucceeded.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
status: action.payload.status,
|
||||||
|
message: action.payload.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceFailed.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
status: 'error',
|
||||||
|
message: action.payload.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dataSources: dataSourcesReducer,
|
dataSources: dataSourcesReducer,
|
||||||
|
dataSourceSettings: dataSourceSettingsReducer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
|
||||||
import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
|
import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
|
||||||
|
import { GenericDataSourcePlugin } from 'app/features/datasources/settings/PluginSettings';
|
||||||
|
|
||||||
export interface DataSourcesState {
|
export interface DataSourcesState {
|
||||||
dataSources: DataSourceSettings[];
|
dataSources: DataSourceSettings[];
|
||||||
@@ -15,6 +16,15 @@ export interface DataSourcesState {
|
|||||||
categories: DataSourcePluginCategory[];
|
categories: DataSourcePluginCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataSourceSettingsState {
|
||||||
|
plugin?: GenericDataSourcePlugin;
|
||||||
|
testingStatus?: {
|
||||||
|
message?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
loadError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataSourcePluginCategory {
|
export interface DataSourcePluginCategory {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AlertRulesState } from './alerting';
|
|||||||
import { TeamsState, TeamState } from './teams';
|
import { TeamsState, TeamState } from './teams';
|
||||||
import { FolderState } from './folders';
|
import { FolderState } from './folders';
|
||||||
import { DashboardState } from './dashboard';
|
import { DashboardState } from './dashboard';
|
||||||
import { DataSourcesState } from './datasources';
|
import { DataSourcesState, DataSourceSettingsState } from './datasources';
|
||||||
import { ExploreState } from './explore';
|
import { ExploreState } from './explore';
|
||||||
import { UsersState, UserState, UserAdminState } from './user';
|
import { UsersState, UserState, UserAdminState } from './user';
|
||||||
import { OrganizationState } from './organization';
|
import { OrganizationState } from './organization';
|
||||||
@@ -28,6 +28,7 @@ export interface StoreState {
|
|||||||
dashboard: DashboardState;
|
dashboard: DashboardState;
|
||||||
panelEditor: PanelEditorState;
|
panelEditor: PanelEditorState;
|
||||||
dataSources: DataSourcesState;
|
dataSources: DataSourcesState;
|
||||||
|
dataSourceSettings: DataSourceSettingsState;
|
||||||
explore: ExploreState;
|
explore: ExploreState;
|
||||||
users: UsersState;
|
users: UsersState;
|
||||||
organization: OrganizationState;
|
organization: OrganizationState;
|
||||||
|
|||||||
Reference in New Issue
Block a user