Files
grafana/public/app/core/services/backend_srv.ts
Torkel Ödegaard 140ecbcf79 QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899)
* I needed to learn some rxjs and understand this more, so just playing around

* Updated

* Removed all the complete calls

* Refactoring

* StreamHandler -> observable start

* progress

* simple singal works

* Handle update time range

* added error handling

* wrap old function

* minor changes

* handle data format in the subscribe function

* Use replay subject to return last value to subscribers

* Set loading state after no response in 50ms

* added missing file

* updated comment

* Added cancelation of network requests

* runRequest: Added unit test scenario framework

* Progress on tests

* minor refactor of unit tests

* updated test

* removed some old code

* Shared queries work again, and also became so much simplier

* unified query and observe methods

* implict any fix

* Fixed closed subject issue

* removed comment

* Use last returned data for loading state

* WIP: Explore to runRequest makover step1

* Minor progress

* Minor progress on explore and runRequest

* minor progress

* Things are starting to work in explore

* Updated prometheus to use new observable query response, greatly simplified code

* Revert refId change

* Found better solution for key/refId/requestId problem

* use observable with loki

* tests compile

* fix loki query prep

* Explore: correct first response handling

* Refactorings

* Refactoring

* Explore: Fixes LoadingState and GraphResults between runs (#18986)

* Refactor: Adds state to DataQueryResponse

* Fix: Fixes so we do not empty results before new data arrives
Fixes: #17409

* Transformations work

* observable test data

* remove single() from loki promise

* Fixed comment

* Explore: Fixes failing Loki and Prometheus unit tests (#18995)

* Tests: Makes datasource tests work again

* Fix: Fixes loki datasource so highligthing works

* Chore: Runs Prettier

* Fixed query runner tests

* Delay loading state indication to 200ms

* Fixed test

* fixed unit tests

* Clear cached calcs

* Fixed bug getProcesedDataFrames

* Fix the correct test is a better idea

* Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000)

* Fix: Fixes so queries in Explore are only run if Graph/Table is shown
Fixes: #18618

* Refactor: Removes unnecessary condition

* PanelData: provide legacy data only when needed  (#19018)

* no legacy

* invert logic... now compiles

* merge getQueryResponseData and getDataRaw

* update comment about query editor

* use single getData() function

* only send legacy when it is used in explore

* pre process rather than post process

* pre process rather than post process

* Minor refactoring

* Add missing tags to test datasource response

* MixedDatasource: Adds query observable pattern to MixedDatasource (#19037)

* start mixed datasource

* Refactor: Refactors into observable parttern

* Tests: Fixes tests

* Tests: Removes console.log

* Refactor: Adds unique requestId
2019-09-12 17:28:46 +02:00

395 lines
11 KiB
TypeScript

import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search';
import { ContextSrv } from './context_srv';
import { FolderInfo, DashboardDTO } from 'app/types';
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
export class BackendSrv implements BackendService {
private inFlightRequests: { [key: string]: Array<angular.IDeferred<any>> } = {};
private HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean;
/** @ngInject */
constructor(
private $http: any,
private $q: angular.IQService,
private $timeout: angular.ITimeoutService,
private contextSrv: ContextSrv
) {}
get(url: string, params?: any) {
return this.request({ method: 'GET', url, params });
}
delete(url: string) {
return this.request({ method: 'DELETE', url });
}
post(url: string, data?: any) {
return this.request({ method: 'POST', url, data });
}
patch(url: string, data: any) {
return this.request({ method: 'PATCH', url, data });
}
put(url: string, data: any) {
return this.request({ method: 'PUT', url, data });
}
withNoBackendCache(callback: any) {
this.noBackendCache = true;
return callback().finally(() => {
this.noBackendCache = false;
});
}
requestErrorHandler(err: any) {
if (err.isHandled) {
return;
}
let data = err.data || { message: 'Unexpected error' };
if (_.isString(data)) {
data = { message: data };
}
if (err.status === 422) {
appEvents.emit('alert-warning', ['Validation failed', data.message]);
throw data;
}
let severity = 'error';
if (err.status < 500) {
severity = 'warning';
}
if (data.message) {
let description = '';
let message = data.message;
if (message.length > 80) {
description = message;
message = 'Error';
}
appEvents.emit('alert-' + severity, [message, description]);
}
throw data;
}
request(options: BackendSrvRequest) {
options.retry = options.retry || 0;
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
}
return this.$http(options).then(
(results: any) => {
if (options.method !== 'GET') {
if (results && results.data.message) {
if (options.showSuccessAlert !== false) {
appEvents.emit('alert-success', [results.data.message]);
}
}
}
return results.data;
},
(err: any) => {
// handle unauthorized
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing()
.then(() => {
options.retry = 1;
return this.request(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
throw err;
}
);
}
addCanceler(requestId: string, canceler: angular.IDeferred<any>) {
if (requestId in this.inFlightRequests) {
this.inFlightRequests[requestId].push(canceler);
} else {
this.inFlightRequests[requestId] = [canceler];
}
}
resolveCancelerIfExists(requestId: string) {
const cancelers = this.inFlightRequests[requestId];
if (!_.isUndefined(cancelers) && cancelers.length) {
cancelers[0].resolve();
}
}
datasourceRequest(options: any) {
let canceler: angular.IDeferred<any> = null;
options.retry = options.retry || 0;
// A requestID is provided by the datasource as a unique identifier for a
// particular query. If the requestID exists, the promise it is keyed to
// is canceled, canceling the previous datasource request if it is still
// in-flight.
const requestId = options.requestId;
if (requestId) {
this.resolveCancelerIfExists(requestId);
// create new canceler
canceler = this.$q.defer();
options.timeout = canceler.promise;
this.addCanceler(requestId, canceler);
}
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
if (options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
if (this.noBackendCache) {
options.headers['X-Grafana-NoCache'] = 'true';
}
}
return this.$http(options)
.then((response: any) => {
if (!options.silent) {
appEvents.emit('ds-request-response', response);
}
return response;
})
.catch((err: any) => {
if (err.status === this.HTTP_REQUEST_CANCELED) {
throw { err, cancelled: true };
}
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {
return this.loginPing()
.then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
// populate error obj on Internal Error
if (_.isString(err.data) && err.status === 500) {
err.data = {
error: err.statusText,
response: err.data,
};
}
// for Prometheus
if (err.data && !err.data.message && _.isString(err.data.error)) {
err.data.message = err.data.error;
}
if (!options.silent) {
appEvents.emit('ds-request-error', err);
}
throw err;
})
.finally(() => {
// clean up
if (options.requestId) {
this.inFlightRequests[options.requestId].shift();
}
});
}
loginPing() {
return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
}
search(query: any): Promise<DashboardSearchHit[]> {
return this.get('/api/search', query);
}
getDashboardBySlug(slug: string) {
return this.get(`/api/dashboards/db/${slug}`);
}
getDashboardByUid(uid: string) {
return this.get(`/api/dashboards/uid/${uid}`);
}
getFolderByUid(uid: string) {
return this.get(`/api/folders/${uid}`);
}
saveDashboard(
dash: DashboardModel,
{ message = '', folderId, overwrite = false }: { message?: string; folderId?: number; overwrite?: boolean } = {}
) {
return this.post('/api/dashboards/db/', {
dashboard: dash,
folderId,
overwrite,
message,
});
}
createFolder(payload: any) {
return this.post('/api/folders', payload);
}
deleteFolder(uid: string, showSuccessAlert: boolean) {
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
}
deleteDashboard(uid: string, showSuccessAlert: boolean) {
return this.request({
method: 'DELETE',
url: `/api/dashboards/uid/${uid}`,
showSuccessAlert: showSuccessAlert === true,
});
}
deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
const tasks = [];
for (const folderUid of folderUids) {
tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true));
}
for (const dashboardUid of dashboardUids) {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
}
return this.executeInOrder(tasks, []);
}
moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
const tasks = [];
for (const uid of dashboardUids) {
tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder));
}
return this.executeInOrder(tasks, []).then((result: any) => {
return {
totalCount: result.length,
successCount: _.filter(result, { succeeded: true }).length,
alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length,
};
});
}
private moveDashboard(uid: string, toFolder: FolderInfo) {
const deferred = this.$q.defer();
this.getDashboardByUid(uid).then((fullDash: DashboardDTO) => {
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
deferred.resolve({ alreadyInFolder: true });
return;
}
const clone = model.getSaveModelClone();
const options = {
folderId: toFolder.id,
overwrite: false,
};
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
if (err.data && err.data.status === 'plugin-dashboard') {
err.isHandled = true;
options.overwrite = true;
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
deferred.resolve({ succeeded: false });
});
} else {
deferred.resolve({ succeeded: false });
}
});
});
return deferred.promise;
}
private createTask(fn: Function, ignoreRejections: boolean, ...args: any[]) {
return (result: any) => {
return fn
.apply(null, args)
.then((res: any) => {
return Array.prototype.concat(result, [res]);
})
.catch((err: any) => {
if (ignoreRejections) {
return result;
}
throw err;
});
};
}
private executeInOrder(tasks: any[], initialValue: any[]) {
return tasks.reduce(this.$q.when, initialValue);
}
}
coreModule.service('backendSrv', BackendSrv);
// Used for testing and things that really need BackendSrv
export function getBackendSrv(): BackendSrv {
return getBackendService() as BackendSrv;
}