grafana/public/app/features/datasources/state/actions.ts
Ashley Harrison 13e3308959
Chore: some type fixes (#69860)
* some type fixes

* restore empty object

* undo 1 fix for now

* commit betterer update

* explicitly type slug and uid as string | undefined
2023-06-20 17:13:49 +01:00

312 lines
10 KiB
TypeScript

import { DataSourcePluginMeta, DataSourceSettings, locationUtil, TestDataSourceResponse } from '@grafana/data';
import {
config,
DataSourceWithBackend,
getDataSourceSrv,
HealthCheckError,
HealthCheckResultDetails,
isFetchError,
locationService,
} from '@grafana/runtime';
import { updateNavIndex } from 'app/core/actions';
import { 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 { 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: typeof getDataSourceSrv;
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<void> => {
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<void> => {
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,
});
} 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,
});
}
});
};
};
export function loadDataSources(): ThunkResult<Promise<void>> {
return async (dispatch) => {
dispatch(dataSourcesLoad());
const response = await api.getDataSources();
dispatch(dataSourcesLoaded(response));
};
}
export function loadDataSource(uid: string): ThunkResult<Promise<DataSourceSettings>> {
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<void> {
return async (dispatch) => {
const pluginInfo = (await getPluginSettings(dataSource.type)) as DataSourcePluginMeta;
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<Promise<void>> {
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<void> {
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<Promise<DataSourceSettings>> | { 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<void> {
return async (dispatch, getStore) => {
const { uid } = getStore().dataSources.dataSource;
try {
await api.deleteDataSource(uid);
await getDatasourceSrv().reload();
const datasourcesUrl = config.featureToggles.dataConnectionsConsole
? CONNECTIONS_ROUTES.DataSources
: '/datasources';
locationService.push(datasourcesUrl);
} catch (err) {
const formattedError = parseHealthCheckError(err);
dispatch(testDataSourceFailed(formattedError));
}
};
}