import { DataSourcePluginMeta, DataSourceSettings, locationUtil, TestDataSourceResponse, DataSourceTestSucceeded, DataSourceTestFailed, } from '@grafana/data'; import { config, DataSourceSrv, DataSourceWithBackend, HealthCheckError, HealthCheckResultDetails, isFetchError, locationService, } from '@grafana/runtime'; import { updateNavIndex } from 'app/core/actions'; import { appEvents, contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; import { AccessControlAction, DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types'; import * as api from '../api'; import { DATASOURCES_ROUTES } from '../constants'; import { trackDataSourceCreated, trackDataSourceTested } from '../tracking'; import { findNewName, nameExits } from '../utils'; import { buildCategories } from './buildCategories'; import { buildNavModel } from './navModel'; import { dataSourceLoaded, dataSourceMetaLoaded, dataSourcePluginsLoad, dataSourcePluginsLoaded, dataSourcesLoad, dataSourcesLoaded, initDataSourceSettingsFailed, initDataSourceSettingsSucceeded, testDataSourceFailed, testDataSourceStarting, testDataSourceSucceeded, } from './reducers'; import { getDataSource, getDataSourceMeta } from './selectors'; export interface DataSourceTypesLoadedPayload { plugins: DataSourcePluginMeta[]; categories: DataSourcePluginCategory[]; } export interface InitDataSourceSettingDependencies { loadDataSource: typeof loadDataSource; loadDataSourceMeta: typeof loadDataSourceMeta; getDataSource: typeof getDataSource; getDataSourceMeta: typeof getDataSourceMeta; importDataSourcePlugin: typeof importDataSourcePlugin; } export interface TestDataSourceDependencies { getDatasourceSrv: () => Pick; getBackendSrv: typeof getBackendSrv; } type parseDataSourceSaveResponse = { message?: string | undefined; status?: string; details?: HealthCheckResultDetails | { message?: string; verboseMessage?: string }; }; const parseHealthCheckError = (errorResponse: any): parseDataSourceSaveResponse => { let message: string | undefined; let details: HealthCheckResultDetails; if (errorResponse.error && errorResponse.error instanceof HealthCheckError) { message = errorResponse.error.message; details = errorResponse.error.details; } else if (isFetchError(errorResponse)) { message = errorResponse.data.message ?? `HTTP error ${errorResponse.statusText}`; } else if (errorResponse instanceof Error) { message = errorResponse.message; } return { message, details }; }; const parseHealthCheckSuccess = (response: TestDataSourceResponse): parseDataSourceSaveResponse => { const { details, message, status } = response; return { status, message, details }; }; export const initDataSourceSettings = ( uid: string, dependencies: InitDataSourceSettingDependencies = { loadDataSource, loadDataSourceMeta, getDataSource, getDataSourceMeta, importDataSourcePlugin, } ): ThunkResult => { return async (dispatch, getState) => { if (!uid) { dispatch(initDataSourceSettingsFailed(new Error('Invalid UID'))); return; } try { const loadedDataSource = await dispatch(dependencies.loadDataSource(uid)); await dispatch(dependencies.loadDataSourceMeta(loadedDataSource)); const dataSource = dependencies.getDataSource(getState().dataSources, uid); const dataSourceMeta = dependencies.getDataSourceMeta(getState().dataSources, dataSource!.type); const importedPlugin = await dependencies.importDataSourcePlugin(dataSourceMeta); dispatch(initDataSourceSettingsSucceeded(importedPlugin)); } catch (err) { if (err instanceof Error) { dispatch(initDataSourceSettingsFailed(err)); } } }; }; export const testDataSource = ( dataSourceName: string, editRoute = DATASOURCES_ROUTES.Edit, dependencies: TestDataSourceDependencies = { getDatasourceSrv, getBackendSrv, } ): ThunkResult => { return async (dispatch: ThunkDispatch, getState) => { const dsApi = await dependencies.getDatasourceSrv().get(dataSourceName); const editLink = editRoute.replace(/:uid/gi, dataSourceName); if (!dsApi.testDatasource) { return; } dispatch(testDataSourceStarting()); dependencies.getBackendSrv().withNoBackendCache(async () => { try { const result = await dsApi.testDatasource(); const parsedResult = parseHealthCheckSuccess({ ...result, details: { ...result.details } }); dispatch(testDataSourceSucceeded(parsedResult)); trackDataSourceTested({ grafana_version: config.buildInfo.version, plugin_id: dsApi.type, datasource_uid: dsApi.uid, success: true, path: editLink, }); appEvents.publish(new DataSourceTestSucceeded()); } catch (err) { const formattedError = parseHealthCheckError(err); dispatch(testDataSourceFailed({ ...formattedError })); trackDataSourceTested({ grafana_version: config.buildInfo.version, plugin_id: dsApi.type, datasource_uid: dsApi.uid, success: false, path: editLink, }); appEvents.publish(new DataSourceTestFailed()); } }); }; }; export function loadDataSources(): ThunkResult> { return async (dispatch) => { if (!contextSrv.hasPermission(AccessControlAction.DataSourcesRead)) { return; } dispatch(dataSourcesLoad()); const response = await api.getDataSources(); dispatch(dataSourcesLoaded(response)); }; } export function loadDataSource(uid: string): ThunkResult> { return async (dispatch) => { let dataSource = await api.getDataSourceByIdOrUid(uid); // Reload route to use UID instead // ------------------------------- // In case we were trying to fetch and reference a data-source with an old numeric ID // (which can happen by referencing it with a "old" URL), we would like to automatically redirect // to the new URL format using the UID. // [Please revalidate the following]: Unfortunately we can update the location using react router, but need to fully reload the // route as the nav model page index is not matching with the url in that case. // And react router has no way to unmount remount a route. if (uid !== dataSource.uid) { window.location.href = locationUtil.assureBaseUrl(`/datasources/edit/${dataSource.uid}`); // Avoid a flashing error while the reload happens dataSource = {} as DataSourceSettings; } dispatch(dataSourceLoaded(dataSource)); return dataSource; }; } export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult { return async (dispatch) => { const pluginInfo: DataSourcePluginMeta = await getPluginSettings(dataSource.type); const plugin = await importDataSourcePlugin(pluginInfo); const isBackend = plugin.DataSourceClass.prototype instanceof DataSourceWithBackend; const meta = { ...pluginInfo, isBackend: pluginInfo.backend || isBackend, }; dispatch(dataSourceMetaLoaded(meta)); plugin.meta = meta; dispatch(updateNavIndex(buildNavModel(dataSource, plugin))); }; } export function addDataSource( plugin: DataSourcePluginMeta, editRoute = DATASOURCES_ROUTES.Edit ): ThunkResult> { return async (dispatch, getStore) => { // update the list of datasources first. // We later use this list to check whether the name of the datasource // being created is unuque or not and assign a new name to it if needed. const response = await api.getDataSources(); dispatch(dataSourcesLoaded(response)); const dataSources = getStore().dataSources.dataSources; const isFirstDataSource = dataSources.length === 0; const newInstance = { name: plugin.name, type: plugin.id, access: 'proxy', isDefault: isFirstDataSource, }; // TODO: typo in name if (nameExits(dataSources, newInstance.name)) { newInstance.name = findNewName(dataSources, newInstance.name); } const result = await api.createDataSource(newInstance); const editLink = editRoute.replace(/:uid/gi, result.datasource.uid); await getDatasourceSrv().reload(); await contextSrv.fetchUserPermissions(); trackDataSourceCreated({ grafana_version: config.buildInfo.version, plugin_id: plugin.id, datasource_uid: result.datasource.uid, plugin_version: result.meta?.info?.version, path: editLink, }); locationService.push(editLink); }; } export function loadDataSourcePlugins(): ThunkResult { return async (dispatch) => { dispatch(dataSourcePluginsLoad()); const plugins = await api.getDataSourcePlugins(); const categories = buildCategories(plugins); dispatch(dataSourcePluginsLoaded({ plugins, categories })); }; } export function updateDataSource(dataSource: DataSourceSettings) { return async ( dispatch: ( dataSourceSettings: ThunkResult> | { payload: unknown; type: string } ) => DataSourceSettings ) => { try { await api.updateDataSource(dataSource); } catch (err) { const formattedError = parseHealthCheckError(err); dispatch(testDataSourceFailed(formattedError)); return Promise.reject(dataSource); } await getDatasourceSrv().reload(); return dispatch(loadDataSource(dataSource.uid)); }; } export function deleteLoadedDataSource(): ThunkResult { return async (dispatch, getStore) => { const { uid } = getStore().dataSources.dataSource; try { await api.deleteDataSource(uid); await getDatasourceSrv().reload(); const datasourcesUrl = CONNECTIONS_ROUTES.DataSources; locationService.push(datasourcesUrl); } catch (err) { const formattedError = parseHealthCheckError(err); dispatch(testDataSourceFailed(formattedError)); } }; }