backend_srv.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import _ from 'lodash';
  2. import coreModule from 'app/core/core_module';
  3. import appEvents from 'app/core/app_events';
  4. import config from 'app/core/config';
  5. import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
  6. import { DashboardSearchHit } from 'app/types/search';
  7. export class BackendSrv {
  8. private inFlightRequests = {};
  9. private HTTP_REQUEST_CANCELED = -1;
  10. private noBackendCache: boolean;
  11. /** @ngInject */
  12. constructor(private $http, private $q, private $timeout, private contextSrv) {}
  13. get(url, params?) {
  14. return this.request({ method: 'GET', url: url, params: params });
  15. }
  16. delete(url) {
  17. return this.request({ method: 'DELETE', url: url });
  18. }
  19. post(url, data) {
  20. return this.request({ method: 'POST', url: url, data: data });
  21. }
  22. patch(url, data) {
  23. return this.request({ method: 'PATCH', url: url, data: data });
  24. }
  25. put(url, data) {
  26. return this.request({ method: 'PUT', url: url, data: data });
  27. }
  28. withNoBackendCache(callback) {
  29. this.noBackendCache = true;
  30. return callback().finally(() => {
  31. this.noBackendCache = false;
  32. });
  33. }
  34. requestErrorHandler(err) {
  35. if (err.isHandled) {
  36. return;
  37. }
  38. let data = err.data || { message: 'Unexpected error' };
  39. if (_.isString(data)) {
  40. data = { message: data };
  41. }
  42. if (err.status === 422) {
  43. appEvents.emit('alert-warning', ['Validation failed', data.message]);
  44. throw data;
  45. }
  46. let severity = 'error';
  47. if (err.status < 500) {
  48. severity = 'warning';
  49. }
  50. if (data.message) {
  51. let description = '';
  52. let message = data.message;
  53. if (message.length > 80) {
  54. description = message;
  55. message = 'Error';
  56. }
  57. appEvents.emit('alert-' + severity, [message, description]);
  58. }
  59. throw data;
  60. }
  61. request(options) {
  62. options.retry = options.retry || 0;
  63. const requestIsLocal = !options.url.match(/^http/);
  64. const firstAttempt = options.retry === 0;
  65. if (requestIsLocal) {
  66. if (this.contextSrv.user && this.contextSrv.user.orgId) {
  67. options.headers = options.headers || {};
  68. options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
  69. }
  70. if (options.url.indexOf('/') === 0) {
  71. options.url = options.url.substring(1);
  72. }
  73. }
  74. return this.$http(options).then(
  75. results => {
  76. if (options.method !== 'GET') {
  77. if (results && results.data.message) {
  78. if (options.showSuccessAlert !== false) {
  79. appEvents.emit('alert-success', [results.data.message]);
  80. }
  81. }
  82. }
  83. return results.data;
  84. },
  85. err => {
  86. // handle unauthorized
  87. if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
  88. return this.loginPing()
  89. .then(() => {
  90. options.retry = 1;
  91. return this.request(options);
  92. })
  93. .catch(err => {
  94. if (err.status === 401) {
  95. window.location.href = config.appSubUrl + '/logout';
  96. throw err;
  97. }
  98. });
  99. }
  100. this.$timeout(this.requestErrorHandler.bind(this, err), 50);
  101. throw err;
  102. }
  103. );
  104. }
  105. addCanceler(requestId, canceler) {
  106. if (requestId in this.inFlightRequests) {
  107. this.inFlightRequests[requestId].push(canceler);
  108. } else {
  109. this.inFlightRequests[requestId] = [canceler];
  110. }
  111. }
  112. resolveCancelerIfExists(requestId) {
  113. const cancelers = this.inFlightRequests[requestId];
  114. if (!_.isUndefined(cancelers) && cancelers.length) {
  115. cancelers[0].resolve();
  116. }
  117. }
  118. datasourceRequest(options) {
  119. let canceler = null;
  120. options.retry = options.retry || 0;
  121. // A requestID is provided by the datasource as a unique identifier for a
  122. // particular query. If the requestID exists, the promise it is keyed to
  123. // is canceled, canceling the previous datasource request if it is still
  124. // in-flight.
  125. const requestId = options.requestId;
  126. if (requestId) {
  127. this.resolveCancelerIfExists(requestId);
  128. // create new canceler
  129. canceler = this.$q.defer();
  130. options.timeout = canceler.promise;
  131. this.addCanceler(requestId, canceler);
  132. }
  133. const requestIsLocal = !options.url.match(/^http/);
  134. const firstAttempt = options.retry === 0;
  135. if (requestIsLocal) {
  136. if (this.contextSrv.user && this.contextSrv.user.orgId) {
  137. options.headers = options.headers || {};
  138. options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
  139. }
  140. if (options.url.indexOf('/') === 0) {
  141. options.url = options.url.substring(1);
  142. }
  143. if (options.headers && options.headers.Authorization) {
  144. options.headers['X-DS-Authorization'] = options.headers.Authorization;
  145. delete options.headers.Authorization;
  146. }
  147. if (this.noBackendCache) {
  148. options.headers['X-Grafana-NoCache'] = 'true';
  149. }
  150. }
  151. return this.$http(options)
  152. .then(response => {
  153. if (!options.silent) {
  154. appEvents.emit('ds-request-response', response);
  155. }
  156. return response;
  157. })
  158. .catch(err => {
  159. if (err.status === this.HTTP_REQUEST_CANCELED) {
  160. throw { err, cancelled: true };
  161. }
  162. // handle unauthorized for backend requests
  163. if (requestIsLocal && firstAttempt && err.status === 401) {
  164. return this.loginPing()
  165. .then(() => {
  166. options.retry = 1;
  167. if (canceler) {
  168. canceler.resolve();
  169. }
  170. return this.datasourceRequest(options);
  171. })
  172. .catch(err => {
  173. if (err.status === 401) {
  174. window.location.href = config.appSubUrl + '/logout';
  175. throw err;
  176. }
  177. });
  178. }
  179. // populate error obj on Internal Error
  180. if (_.isString(err.data) && err.status === 500) {
  181. err.data = {
  182. error: err.statusText,
  183. response: err.data,
  184. };
  185. }
  186. // for Prometheus
  187. if (err.data && !err.data.message && _.isString(err.data.error)) {
  188. err.data.message = err.data.error;
  189. }
  190. if (!options.silent) {
  191. appEvents.emit('ds-request-error', err);
  192. }
  193. throw err;
  194. })
  195. .finally(() => {
  196. // clean up
  197. if (options.requestId) {
  198. this.inFlightRequests[options.requestId].shift();
  199. }
  200. });
  201. }
  202. loginPing() {
  203. return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
  204. }
  205. search(query): Promise<DashboardSearchHit[]> {
  206. return this.get('/api/search', query);
  207. }
  208. getDashboardBySlug(slug) {
  209. return this.get(`/api/dashboards/db/${slug}`);
  210. }
  211. getDashboardByUid(uid: string) {
  212. return this.get(`/api/dashboards/uid/${uid}`);
  213. }
  214. getFolderByUid(uid: string) {
  215. return this.get(`/api/folders/${uid}`);
  216. }
  217. saveDashboard(dash, options) {
  218. options = options || {};
  219. return this.post('/api/dashboards/db/', {
  220. dashboard: dash,
  221. folderId: options.folderId,
  222. overwrite: options.overwrite === true,
  223. message: options.message || '',
  224. });
  225. }
  226. createFolder(payload: any) {
  227. return this.post('/api/folders', payload);
  228. }
  229. deleteFolder(uid: string, showSuccessAlert) {
  230. return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
  231. }
  232. deleteDashboard(uid, showSuccessAlert) {
  233. return this.request({
  234. method: 'DELETE',
  235. url: `/api/dashboards/uid/${uid}`,
  236. showSuccessAlert: showSuccessAlert === true,
  237. });
  238. }
  239. deleteFoldersAndDashboards(folderUids, dashboardUids) {
  240. const tasks = [];
  241. for (const folderUid of folderUids) {
  242. tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true));
  243. }
  244. for (const dashboardUid of dashboardUids) {
  245. tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
  246. }
  247. return this.executeInOrder(tasks, []);
  248. }
  249. moveDashboards(dashboardUids, toFolder) {
  250. const tasks = [];
  251. for (const uid of dashboardUids) {
  252. tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder));
  253. }
  254. return this.executeInOrder(tasks, []).then(result => {
  255. return {
  256. totalCount: result.length,
  257. successCount: _.filter(result, { succeeded: true }).length,
  258. alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length,
  259. };
  260. });
  261. }
  262. private moveDashboard(uid, toFolder) {
  263. const deferred = this.$q.defer();
  264. this.getDashboardByUid(uid).then(fullDash => {
  265. const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
  266. if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
  267. deferred.resolve({ alreadyInFolder: true });
  268. return;
  269. }
  270. const clone = model.getSaveModelClone();
  271. const options = {
  272. folderId: toFolder.id,
  273. overwrite: false,
  274. };
  275. this.saveDashboard(clone, options)
  276. .then(() => {
  277. deferred.resolve({ succeeded: true });
  278. })
  279. .catch(err => {
  280. if (err.data && err.data.status === 'plugin-dashboard') {
  281. err.isHandled = true;
  282. options.overwrite = true;
  283. this.saveDashboard(clone, options)
  284. .then(() => {
  285. deferred.resolve({ succeeded: true });
  286. })
  287. .catch(err => {
  288. deferred.resolve({ succeeded: false });
  289. });
  290. } else {
  291. deferred.resolve({ succeeded: false });
  292. }
  293. });
  294. });
  295. return deferred.promise;
  296. }
  297. private createTask(fn, ignoreRejections, ...args: any[]) {
  298. return result => {
  299. return fn
  300. .apply(null, args)
  301. .then(res => {
  302. return Array.prototype.concat(result, [res]);
  303. })
  304. .catch(err => {
  305. if (ignoreRejections) {
  306. return result;
  307. }
  308. throw err;
  309. });
  310. };
  311. }
  312. private executeInOrder(tasks, initialValue) {
  313. return tasks.reduce(this.$q.when, initialValue);
  314. }
  315. }
  316. coreModule.service('backendSrv', BackendSrv);
  317. //
  318. // Code below is to expore the service to react components
  319. //
  320. let singletonInstance: BackendSrv;
  321. export function setBackendSrv(instance: BackendSrv) {
  322. singletonInstance = instance;
  323. }
  324. export function getBackendSrv(): BackendSrv {
  325. return singletonInstance;
  326. }