actions.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { lastValueFrom } from 'rxjs';
  2. import { DataSourcePluginMeta, DataSourceSettings, locationUtil } from '@grafana/data';
  3. import { DataSourceWithBackend, getDataSourceSrv, locationService } from '@grafana/runtime';
  4. import { updateNavIndex } from 'app/core/actions';
  5. import { getBackendSrv } from 'app/core/services/backend_srv';
  6. import { accessControlQueryParam } from 'app/core/utils/accessControl';
  7. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  8. import { getPluginSettings } from 'app/features/plugins/pluginSettings';
  9. import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
  10. import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
  11. import { contextSrv } from '../../../core/services/context_srv';
  12. import { buildCategories } from './buildCategories';
  13. import { buildNavModel } from './navModel';
  14. import {
  15. dataSourceLoaded,
  16. dataSourceMetaLoaded,
  17. dataSourcePluginsLoad,
  18. dataSourcePluginsLoaded,
  19. dataSourcesLoaded,
  20. initDataSourceSettingsFailed,
  21. initDataSourceSettingsSucceeded,
  22. testDataSourceFailed,
  23. testDataSourceStarting,
  24. testDataSourceSucceeded,
  25. } from './reducers';
  26. import { getDataSource, getDataSourceMeta } from './selectors';
  27. export interface DataSourceTypesLoadedPayload {
  28. plugins: DataSourcePluginMeta[];
  29. categories: DataSourcePluginCategory[];
  30. }
  31. export interface InitDataSourceSettingDependencies {
  32. loadDataSource: typeof loadDataSource;
  33. loadDataSourceMeta: typeof loadDataSourceMeta;
  34. getDataSource: typeof getDataSource;
  35. getDataSourceMeta: typeof getDataSourceMeta;
  36. importDataSourcePlugin: typeof importDataSourcePlugin;
  37. }
  38. export interface TestDataSourceDependencies {
  39. getDatasourceSrv: typeof getDataSourceSrv;
  40. getBackendSrv: typeof getBackendSrv;
  41. }
  42. export const initDataSourceSettings = (
  43. pageId: string,
  44. dependencies: InitDataSourceSettingDependencies = {
  45. loadDataSource,
  46. loadDataSourceMeta,
  47. getDataSource,
  48. getDataSourceMeta,
  49. importDataSourcePlugin,
  50. }
  51. ): ThunkResult<void> => {
  52. return async (dispatch, getState) => {
  53. if (!pageId) {
  54. dispatch(initDataSourceSettingsFailed(new Error('Invalid ID')));
  55. return;
  56. }
  57. try {
  58. const loadedDataSource = await dispatch(dependencies.loadDataSource(pageId));
  59. await dispatch(dependencies.loadDataSourceMeta(loadedDataSource));
  60. // have we already loaded the plugin then we can skip the steps below?
  61. if (getState().dataSourceSettings.plugin) {
  62. return;
  63. }
  64. const dataSource = dependencies.getDataSource(getState().dataSources, pageId);
  65. const dataSourceMeta = dependencies.getDataSourceMeta(getState().dataSources, dataSource!.type);
  66. const importedPlugin = await dependencies.importDataSourcePlugin(dataSourceMeta);
  67. dispatch(initDataSourceSettingsSucceeded(importedPlugin));
  68. } catch (err) {
  69. dispatch(initDataSourceSettingsFailed(err));
  70. }
  71. };
  72. };
  73. export const testDataSource = (
  74. dataSourceName: string,
  75. dependencies: TestDataSourceDependencies = {
  76. getDatasourceSrv,
  77. getBackendSrv,
  78. }
  79. ): ThunkResult<void> => {
  80. return async (dispatch: ThunkDispatch, getState) => {
  81. const dsApi = await dependencies.getDatasourceSrv().get(dataSourceName);
  82. if (!dsApi.testDatasource) {
  83. return;
  84. }
  85. dispatch(testDataSourceStarting());
  86. dependencies.getBackendSrv().withNoBackendCache(async () => {
  87. try {
  88. const result = await dsApi.testDatasource();
  89. dispatch(testDataSourceSucceeded(result));
  90. } catch (err) {
  91. const { statusText, message: errMessage, details, data } = err;
  92. const message = errMessage || data?.message || 'HTTP error ' + statusText;
  93. dispatch(testDataSourceFailed({ message, details }));
  94. }
  95. });
  96. };
  97. };
  98. export function loadDataSources(): ThunkResult<void> {
  99. return async (dispatch) => {
  100. const response = await getBackendSrv().get('/api/datasources');
  101. dispatch(dataSourcesLoaded(response));
  102. };
  103. }
  104. export function loadDataSource(uid: string): ThunkResult<Promise<DataSourceSettings>> {
  105. return async (dispatch) => {
  106. const dataSource = await getDataSourceUsingUidOrId(uid);
  107. dispatch(dataSourceLoaded(dataSource));
  108. return dataSource;
  109. };
  110. }
  111. export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<void> {
  112. return async (dispatch) => {
  113. const pluginInfo = (await getPluginSettings(dataSource.type)) as DataSourcePluginMeta;
  114. const plugin = await importDataSourcePlugin(pluginInfo);
  115. const isBackend = plugin.DataSourceClass.prototype instanceof DataSourceWithBackend;
  116. const meta = {
  117. ...pluginInfo,
  118. isBackend: pluginInfo.backend || isBackend,
  119. };
  120. dispatch(dataSourceMetaLoaded(meta));
  121. plugin.meta = meta;
  122. dispatch(updateNavIndex(buildNavModel(dataSource, plugin)));
  123. };
  124. }
  125. /**
  126. * Get data source by uid or id, if old id detected handles redirect
  127. */
  128. export async function getDataSourceUsingUidOrId(uid: string | number): Promise<DataSourceSettings> {
  129. // Try first with uid api
  130. try {
  131. const byUid = await lastValueFrom(
  132. getBackendSrv().fetch<DataSourceSettings>({
  133. method: 'GET',
  134. url: `/api/datasources/uid/${uid}`,
  135. params: accessControlQueryParam(),
  136. showErrorAlert: false,
  137. })
  138. );
  139. if (byUid.ok) {
  140. return byUid.data;
  141. }
  142. } catch (err) {
  143. console.log('Failed to lookup data source by uid', err);
  144. }
  145. // try lookup by old db id
  146. const id = typeof uid === 'string' ? parseInt(uid, 10) : uid;
  147. if (!Number.isNaN(id)) {
  148. const response = await lastValueFrom(
  149. getBackendSrv().fetch<DataSourceSettings>({
  150. method: 'GET',
  151. url: `/api/datasources/${id}`,
  152. params: accessControlQueryParam(),
  153. showErrorAlert: false,
  154. })
  155. );
  156. // If the uid is a number, then this is a refresh on one of the settings tabs
  157. // and we can return the response data
  158. if (response.ok && typeof uid === 'number' && response.data.id === uid) {
  159. return response.data;
  160. }
  161. // Not ideal to do a full page reload here but so tricky to handle this
  162. // otherwise We can update the location using react router, but need to
  163. // fully reload the route as the nav model page index is not matching with
  164. // the url in that case. And react router has no way to unmount remount a
  165. // route
  166. if (response.ok && response.data.id.toString() === uid) {
  167. window.location.href = locationUtil.assureBaseUrl(`/datasources/edit/${response.data.uid}`);
  168. return {} as DataSourceSettings; // avoids flashing an error
  169. }
  170. }
  171. throw Error('Could not find data source');
  172. }
  173. export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
  174. return async (dispatch, getStore) => {
  175. await dispatch(loadDataSources());
  176. const dataSources = getStore().dataSources.dataSources;
  177. const newInstance = {
  178. name: plugin.name,
  179. type: plugin.id,
  180. access: 'proxy',
  181. isDefault: dataSources.length === 0,
  182. };
  183. if (nameExits(dataSources, newInstance.name)) {
  184. newInstance.name = findNewName(dataSources, newInstance.name);
  185. }
  186. const result = await getBackendSrv().post('/api/datasources', newInstance);
  187. await getDatasourceSrv().reload();
  188. await contextSrv.fetchUserPermissions();
  189. locationService.push(`/datasources/edit/${result.datasource.uid}`);
  190. };
  191. }
  192. export function loadDataSourcePlugins(): ThunkResult<void> {
  193. return async (dispatch) => {
  194. dispatch(dataSourcePluginsLoad());
  195. const plugins = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
  196. const categories = buildCategories(plugins);
  197. dispatch(dataSourcePluginsLoaded({ plugins, categories }));
  198. };
  199. }
  200. export function updateDataSource(dataSource: DataSourceSettings): ThunkResult<void> {
  201. return async (dispatch) => {
  202. await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource); // by UID not yet supported
  203. await getDatasourceSrv().reload();
  204. return dispatch(loadDataSource(dataSource.uid));
  205. };
  206. }
  207. export function deleteDataSource(): ThunkResult<void> {
  208. return async (dispatch, getStore) => {
  209. const dataSource = getStore().dataSources.dataSource;
  210. await getBackendSrv().delete(`/api/datasources/${dataSource.id}`);
  211. await getDatasourceSrv().reload();
  212. locationService.push('/datasources');
  213. };
  214. }
  215. interface ItemWithName {
  216. name: string;
  217. }
  218. export function nameExits(dataSources: ItemWithName[], name: string) {
  219. return (
  220. dataSources.filter((dataSource) => {
  221. return dataSource.name.toLowerCase() === name.toLowerCase();
  222. }).length > 0
  223. );
  224. }
  225. export function findNewName(dataSources: ItemWithName[], name: string) {
  226. // Need to loop through current data sources to make sure
  227. // the name doesn't exist
  228. while (nameExits(dataSources, name)) {
  229. // If there's a duplicate name that doesn't end with '-x'
  230. // we can add -1 to the name and be done.
  231. if (!nameHasSuffix(name)) {
  232. name = `${name}-1`;
  233. } else {
  234. // if there's a duplicate name that ends with '-x'
  235. // we can try to increment the last digit until the name is unique
  236. // remove the 'x' part and replace it with the new number
  237. name = `${getNewName(name)}${incrementLastDigit(getLastDigit(name))}`;
  238. }
  239. }
  240. return name;
  241. }
  242. function nameHasSuffix(name: string) {
  243. return name.endsWith('-', name.length - 1);
  244. }
  245. function getLastDigit(name: string) {
  246. return parseInt(name.slice(-1), 10);
  247. }
  248. function incrementLastDigit(digit: number) {
  249. return isNaN(digit) ? 1 : digit + 1;
  250. }
  251. function getNewName(name: string) {
  252. return name.slice(0, name.length - 1);
  253. }