diff --git a/docs/sources/packages_api/runtime/backendsrv.md b/docs/sources/packages_api/runtime/backendsrv.md
index 335c09196fe..3da6cdb4d0b 100644
--- a/docs/sources/packages_api/runtime/backendsrv.md
+++ b/docs/sources/packages_api/runtime/backendsrv.md
@@ -16,8 +16,9 @@ The request function can be used to perform a remote call by specifying a [Backe
Signature
```typescript
-export interface BackendSrv
+export interface BackendSrv
```
+
Import
```typescript
@@ -26,19 +27,21 @@ import { BackendSrv } from '@grafana/runtime';
## Remarks
-By default Grafana will display an error message alert if the remote call fails. If you want to prevent this from happending you need to catch the error thrown by the BackendSrv and set the `isHandled = true` on the incoming error.
+By default Grafana will display an error message alert if the remote call fails. If you want to prevent this from happending you need to catch the error thrown by the BackendSrv and set the `showErrorAlert = true` on the request options object.
+
+> In versions prior to v7.2 you disable the notification alert by setting isHandled on the caught error
Methods
-| Method | Description |
-| --- | --- |
-| [datasourceRequest(options)](#datasourcerequest-method) | Special function used to communicate with datasources that will emit core events that the Grafana QueryInspector and QueryEditor is listening for to be able to display datasource query information. Can be skipped by adding option.silent when initializing the request. |
-| [delete(url)](#delete-method) | |
-| [get(url, params, requestId)](#get-method) | |
-| [patch(url, data)](#patch-method) | |
-| [post(url, data)](#post-method) | |
-| [put(url, data)](#put-method) | |
-| [request(options)](#request-method) | |
+| Method | Description |
+| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [datasourceRequest(options)](#datasourcerequest-method) | Special function used to communicate with datasources that will emit core events that the Grafana QueryInspector and QueryEditor is listening for to be able to display datasource query information. Can be skipped by adding option.silent when initializing the request. |
+| [delete(url)](#delete-method) | |
+| [get(url, params, requestId)](#get-method) | |
+| [patch(url, data)](#patch-method) | |
+| [post(url, data)](#post-method) | |
+| [put(url, data)](#put-method) | |
+| [request(options)](#request-method) | |
### datasourceRequest method
@@ -49,11 +52,12 @@ Special function used to communicate with datasources that will emit core events
```typescript
datasourceRequest(options: BackendSrvRequest): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| options | BackendSrvRequest | |
+| Parameter | Type | Description |
+| --------- | ------------------------------ | ----------- |
+| options | BackendSrvRequest | |
Returns:
@@ -66,11 +70,12 @@ datasourceRequest(options: BackendSrvRequest): Promise;
```typescript
delete(url: string): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| url | string | |
+| Parameter | Type | Description |
+| --------- | ------------------- | ----------- |
+| url | string | |
Returns:
@@ -83,13 +88,14 @@ delete(url: string): Promise;
```typescript
get(url: string, params?: any, requestId?: string): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| url | string | |
-| params | any | |
-| requestId | string | |
+| Parameter | Type | Description |
+| --------- | ------------------- | ----------- |
+| url | string | |
+| params | any | |
+| requestId | string | |
Returns:
@@ -102,12 +108,13 @@ get(url: string, params?: any, requestId?: string): Promise;
```typescript
patch(url: string, data?: any): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| url | string | |
-| data | any | |
+| Parameter | Type | Description |
+| --------- | ------------------- | ----------- |
+| url | string | |
+| data | any | |
Returns:
@@ -120,12 +127,13 @@ patch(url: string, data?: any): Promise;
```typescript
post(url: string, data?: any): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| url | string | |
-| data | any | |
+| Parameter | Type | Description |
+| --------- | ------------------- | ----------- |
+| url | string | |
+| data | any | |
Returns:
@@ -138,12 +146,13 @@ post(url: string, data?: any): Promise;
```typescript
put(url: string, data?: any): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| url | string | |
-| data | any | |
+| Parameter | Type | Description |
+| --------- | ------------------- | ----------- |
+| url | string | |
+| data | any | |
Returns:
@@ -156,13 +165,13 @@ put(url: string, data?: any): Promise;
```typescript
request(options: BackendSrvRequest): Promise;
```
+
Parameters
-| Parameter | Type | Description |
-| --- | --- | --- |
-| options | BackendSrvRequest | |
+| Parameter | Type | Description |
+| --------- | ------------------------------ | ----------- |
+| options | BackendSrvRequest | |
Returns:
`Promise`
-
diff --git a/packages/grafana-runtime/src/services/backendSrv.ts b/packages/grafana-runtime/src/services/backendSrv.ts
index 77e01fbaff2..472c563db9c 100644
--- a/packages/grafana-runtime/src/services/backendSrv.ts
+++ b/packages/grafana-runtime/src/services/backendSrv.ts
@@ -1,10 +1,16 @@
+import { Observable } from 'rxjs';
+
/**
* Used to initiate a remote call via the {@link BackendSrv}
*
* @public
*/
export type BackendSrvRequest = {
+ /**
+ * Request URL
+ */
url: string;
+
/**
* Number of times to retry the remote call if it fails.
*/
@@ -15,7 +21,7 @@ export type BackendSrvRequest = {
* Please have a look at {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
* for supported headers.
*/
- headers?: any;
+ headers?: Record;
/**
* HTTP verb to perform in the remote call GET, POST, PUT etc.
@@ -23,11 +29,15 @@ export type BackendSrvRequest = {
method?: string;
/**
- * If set to true an alert with the response message will be displayed
- * upon successful remote call
+ * Set to false an success application alert box will not be shown for successful PUT, DELETE, POST requests
*/
showSuccessAlert?: boolean;
+ /**
+ * Set to false to not show an application alert box for request errors
+ */
+ showErrorAlert?: boolean;
+
/**
* Provided by the initiator to identify a particular remote call. An example
* of this is when a datasource plugin triggers a query. If the request id already
@@ -35,9 +45,71 @@ export type BackendSrvRequest = {
* new one.
*/
requestId?: string;
- [key: string]: any;
+
+ /**
+ * Set to to true to not include call in query inspector
+ */
+ silent?: boolean;
+
+ /**
+ * The data to send
+ */
+ data?: any;
+
+ /**
+ * Query params
+ */
+ params?: Record;
+
+ /**
+ * Indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests.
+ * In addition, this flag is also used to indicate when cookies are to be ignored in the response.
+ */
+ withCredentials?: boolean;
};
+/**
+ * Response for fetch function in {@link BackendSrv}
+ *
+ * @public
+ */
+export interface FetchResponse {
+ data: T;
+ readonly status: number;
+ readonly statusText: string;
+ readonly ok: boolean;
+ readonly headers: Headers;
+ readonly redirected: boolean;
+ readonly type: ResponseType;
+ readonly url: string;
+ readonly config: BackendSrvRequest;
+}
+
+/**
+ * Error type for fetch function in {@link BackendSrv}
+ *
+ * @public
+ */
+export interface FetchErrorDataProps {
+ message?: string;
+ status?: string;
+ error?: string | any;
+}
+
+/**
+ * Error type for fetch function in {@link BackendSrv}
+ *
+ * @public
+ */
+export interface FetchError {
+ status: number;
+ statusText?: string;
+ data: T | string;
+ cancelled?: boolean;
+ isHandled?: boolean;
+ config: BackendSrvRequest;
+}
+
/**
* Used to communicate via http(s) to a remote backend such as the Grafana backend,
* a datasource etc. The BackendSrv is using the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
@@ -48,9 +120,8 @@ export type BackendSrvRequest = {
* use default values executing the request.
*
* @remarks
- * By default Grafana will display an error message alert if the remote call fails. If you want
- * to prevent this from happending you need to catch the error thrown by the BackendSrv and
- * set the `isHandled = true` on the incoming error.
+ * By default, Grafana displays an error message alert if the remote call fails. To prevent this from
+ * happening `showErrorAlert = true` on the options object.
*
* @public
*/
@@ -60,15 +131,26 @@ export interface BackendSrv {
post(url: string, data?: any): Promise;
patch(url: string, data?: any): Promise;
put(url: string, data?: any): Promise;
+
+ /**
+ * @deprecated Use the fetch function instead. If you prefer to work with a promise
+ * call the toPromise() function on the Observable returned by fetch.
+ */
request(options: BackendSrvRequest): Promise;
/**
+ * @deprecated Use the fetch function instead
* Special function used to communicate with datasources that will emit core
* events that the Grafana QueryInspector and QueryEditor is listening for to be able
* to display datasource query information. Can be skipped by adding `option.silent`
* when initializing the request.
*/
datasourceRequest(options: BackendSrvRequest): Promise;
+
+ /**
+ * Observable http request interface
+ */
+ fetch(options: BackendSrvRequest): Observable>;
}
let singletonInstance: BackendSrv;
diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
index 270772f0e2a..73503f0bb42 100644
--- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
+++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
@@ -171,12 +171,11 @@ export class DataSourceWithBackend<
*/
async callHealthCheck(): Promise {
return getBackendSrv()
- .get(`/api/datasources/${this.id}/health`)
+ .request({ method: 'GET', url: `/api/datasources/${this.id}/health`, showErrorAlert: false })
.then(v => {
return v as HealthCheckResult;
})
.catch(err => {
- err.isHandled = true; // Avoid extra popup warning
return err.data as HealthCheckResult;
});
}
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts
index 690a45e4278..6b3735fd58a 100644
--- a/public/app/core/services/backend_srv.ts
+++ b/public/app/core/services/backend_srv.ts
@@ -1,58 +1,18 @@
-import { from, merge, MonoTypeOperatorFunction, Observable, of, Subject, throwError } from 'rxjs';
+import { from, merge, MonoTypeOperatorFunction, Observable, Subject, throwError } from 'rxjs';
import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap, throwIfEmpty } from 'rxjs/operators';
import { fromFetch } from 'rxjs/fetch';
-import { BackendSrv as BackendService, BackendSrvRequest } from '@grafana/runtime';
+import { BackendSrv as BackendService, BackendSrvRequest, FetchResponse, FetchError } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
-import { DataSourceResponse } from 'app/types/events';
import { DashboardSearchHit } from 'app/features/search/types';
-import { CoreEvents, DashboardDTO, FolderInfo, DashboardDataDTO, FolderDTO } from 'app/types';
+import { FolderDTO } from 'app/types';
import { coreModule } from 'app/core/core_module';
import { ContextSrv, contextSrv } from './context_srv';
import { Emitter } from '../utils/emitter';
import { parseInitFromOptions, parseUrlFromOptions } from '../utils/fetch';
-export interface DatasourceRequestOptions {
- retry?: number;
- method?: string;
- requestId?: string;
- timeout?: Promise;
- url?: string;
- headers?: Record;
- silent?: boolean;
- data?: Record;
-}
-
-interface FetchResponseProps {
- message?: string;
-}
-
-interface ErrorResponseProps extends FetchResponseProps {
- status?: string;
- error?: string | any;
-}
-
-export interface FetchResponse extends DataSourceResponse {}
-
-interface SuccessResponse extends FetchResponseProps, Record {}
-
-interface DataSourceSuccessResponse extends FetchResponse {}
-
-interface ErrorResponse {
- status: number;
- statusText?: string;
- isHandled?: boolean;
- data: T | string;
- cancelled?: boolean;
-}
-
-enum CancellationType {
- request,
- dataSourceRequest,
-}
-
const CANCEL_ALL_REQUESTS_REQUEST_ID = 'cancel_all_requests_request_id';
export interface BackendSrvDependencies {
@@ -66,6 +26,8 @@ export class BackendSrv implements BackendService {
private inFlightRequests: Subject = new Subject();
private HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean;
+ private inspectorStream: Subject = new Subject();
+
private dependencies: BackendSrvDependencies = {
fromFetch: fromFetch,
appEvents: appEvents,
@@ -84,6 +46,269 @@ export class BackendSrv implements BackendService {
}
}
+ async request(options: BackendSrvRequest): Promise {
+ return this.fetch(options)
+ .pipe(map((response: FetchResponse) => response.data))
+ .toPromise();
+ }
+
+ fetch(options: BackendSrvRequest): Observable> {
+ if (options.requestId) {
+ this.inFlightRequests.next(options.requestId);
+ }
+
+ options = this.parseRequestOptions(options);
+
+ const fromFetchStream = this.getFromFetchStream(options);
+ const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
+ const successStream = fromFetchStream.pipe(
+ filter(response => response.ok === true),
+ tap(response => {
+ this.showSuccessAlert(response);
+ this.inspectorStream.next(response);
+ })
+ );
+
+ return merge(successStream, failureStream).pipe(
+ catchError((err: FetchError) => throwError(this.processRequestError(options, err))),
+ this.handleStreamCancellation(options)
+ );
+ }
+
+ resolveCancelerIfExists(requestId: string) {
+ this.inFlightRequests.next(requestId);
+ }
+
+ cancelAllInFlightRequests() {
+ this.inFlightRequests.next(CANCEL_ALL_REQUESTS_REQUEST_ID);
+ }
+
+ async datasourceRequest(options: BackendSrvRequest): Promise {
+ return this.fetch(options).toPromise();
+ }
+
+ private parseRequestOptions(options: BackendSrvRequest): BackendSrvRequest {
+ const orgId = this.dependencies.contextSrv.user?.orgId;
+
+ // init retry counter
+ options.retry = options.retry ?? 0;
+
+ if (isLocalUrl(options.url)) {
+ if (orgId) {
+ options.headers = options.headers ?? {};
+ options.headers['X-Grafana-Org-Id'] = orgId;
+ }
+
+ if (options.url.startsWith('/')) {
+ options.url = options.url.substring(1);
+ }
+
+ // if (options.url.endsWith('/')) {
+ // options.url = options.url.slice(0, -1);
+ // }
+
+ if (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 options;
+ }
+
+ private getFromFetchStream(options: BackendSrvRequest): Observable> {
+ const url = parseUrlFromOptions(options);
+ const init = parseInitFromOptions(options);
+
+ return this.dependencies.fromFetch(url, init).pipe(
+ mergeMap(async response => {
+ const { status, statusText, ok, headers, url, type, redirected } = response;
+ const textData = await response.text(); // this could be just a string, prometheus requests for instance
+ let data: T;
+
+ try {
+ data = JSON.parse(textData); // majority of the requests this will be something that can be parsed
+ } catch {
+ data = textData as any;
+ }
+
+ const fetchResponse: FetchResponse = {
+ status,
+ statusText,
+ ok,
+ data,
+ headers,
+ url,
+ type,
+ redirected,
+ config: options,
+ };
+ return fetchResponse;
+ }),
+ share() // sharing this so we can split into success and failure and then merge back
+ );
+ }
+
+ private toFailureStream(options: BackendSrvRequest): MonoTypeOperatorFunction> {
+ const { isSignedIn } = this.dependencies.contextSrv.user;
+
+ return inputStream =>
+ inputStream.pipe(
+ filter(response => response.ok === false),
+ mergeMap(response => {
+ const { status, statusText, data } = response;
+ const fetchErrorResponse: FetchError = { status, statusText, data, config: options };
+ return throwError(fetchErrorResponse);
+ }),
+ retryWhen((attempts: Observable) =>
+ attempts.pipe(
+ mergeMap((error, i) => {
+ const firstAttempt = i === 0 && options.retry === 0;
+
+ if (error.status === 401 && isLocalUrl(options.url) && firstAttempt && isSignedIn) {
+ return from(this.loginPing()).pipe(
+ catchError(err => {
+ if (err.status === 401) {
+ this.dependencies.logout();
+ return throwError(err);
+ }
+ return throwError(err);
+ })
+ );
+ }
+
+ return throwError(error);
+ })
+ )
+ )
+ );
+ }
+
+ showApplicationErrorAlert(err: FetchError) {}
+
+ showSuccessAlert(response: FetchResponse) {
+ const { config } = response;
+
+ if (config.showSuccessAlert === false) {
+ return;
+ }
+
+ // is showSuccessAlert is undefined we only show alerts non GET request, non data query and local api requests
+ if (
+ config.showSuccessAlert === undefined &&
+ (config.method === 'GET' || isDataQuery(config.url) || !isLocalUrl(config.url))
+ ) {
+ return;
+ }
+
+ const data: { message: string } = response.data as any;
+
+ if (data?.message) {
+ this.dependencies.appEvents.emit(AppEvents.alertSuccess, [data.message]);
+ }
+ }
+
+ showErrorAlert(config: BackendSrvRequest, err: FetchError) {
+ if (config.showErrorAlert === false) {
+ return;
+ }
+
+ // is showErrorAlert is undefined we only show alerts non data query and local api requests
+ if (config.showErrorAlert === undefined || isDataQuery(config.url) || !isLocalUrl(config.url)) {
+ return;
+ }
+
+ let description = '';
+ let message = err.data.message;
+
+ if (message.length > 80) {
+ description = message;
+ message = 'Error';
+ }
+
+ // Validation
+ if (err.status === 422) {
+ message = 'Validation failed';
+ }
+
+ this.dependencies.appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [
+ message,
+ description,
+ ]);
+ }
+
+ processRequestError(options: BackendSrvRequest, err: FetchError): FetchError {
+ err.data = err.data ?? { message: 'Unexpected error' };
+
+ if (typeof err.data === 'string') {
+ err.data = {
+ error: err.statusText,
+ response: err.data,
+ message: err.data,
+ };
+ }
+
+ // If no message but got error string, copy to message prop
+ if (err.data && !err.data.message && typeof err.data.error === 'string') {
+ err.data.message = err.data.error;
+ }
+
+ // check if we should show an error alert
+ if (err.data.message) {
+ setTimeout(() => {
+ if (!err.isHandled) {
+ this.showErrorAlert(options, err);
+ }
+ }, 50);
+ }
+
+ this.inspectorStream.next(err);
+ return err;
+ }
+
+ private handleStreamCancellation(options: BackendSrvRequest): MonoTypeOperatorFunction> {
+ return inputStream =>
+ inputStream.pipe(
+ takeUntil(
+ this.inFlightRequests.pipe(
+ filter(requestId => {
+ let cancelRequest = false;
+
+ if (options && options.requestId && options.requestId === requestId) {
+ // when a new requestId is started it will be published to inFlightRequests
+ // if a previous long running request that hasn't finished yet has the same requestId
+ // we need to cancel that request
+ cancelRequest = true;
+ }
+
+ if (requestId === CANCEL_ALL_REQUESTS_REQUEST_ID) {
+ cancelRequest = true;
+ }
+
+ return cancelRequest;
+ })
+ )
+ ),
+ // when a request is cancelled by takeUntil it will complete without emitting anything so we use throwIfEmpty to identify this case
+ // in throwIfEmpty we'll then throw an cancelled error and then we'll return the correct result in the catchError or rethrow
+ throwIfEmpty(() => ({
+ cancelled: true,
+ data: null,
+ status: this.HTTP_REQUEST_CANCELED,
+ statusText: 'Request was aborted',
+ config: options,
+ }))
+ );
+ }
+
+ getInspectorStream(): Observable | FetchError> {
+ return this.inspectorStream;
+ }
+
async get(url: string, params?: any, requestId?: string): Promise {
return await this.request({ method: 'GET', url, params, requestId });
}
@@ -111,139 +336,6 @@ export class BackendSrv implements BackendService {
});
}
- requestErrorHandler = (err: ErrorResponse) => {
- if (err.isHandled) {
- return;
- }
-
- let data = err.data ?? { message: 'Unexpected error' };
- if (typeof data === 'string') {
- data = { message: data };
- }
-
- if (err.status === 422) {
- this.dependencies.appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]);
- throw data;
- }
-
- if (data.message) {
- let description = '';
- let message = data.message;
- if (message.length > 80) {
- description = message;
- message = 'Error';
- }
-
- this.dependencies.appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [
- message,
- description,
- ]);
- }
-
- throw data;
- };
-
- async request(options: BackendSrvRequest): Promise {
- // A requestId is a unique identifier for a particular query.
- // Every observable below has a takeUntil that subscribes to this.inFlightRequests and
- // will cancel/unsubscribe that observable when a new datasourceRequest with the same requestId is made
- if (options.requestId) {
- this.inFlightRequests.next(options.requestId);
- }
-
- options = this.parseRequestOptions(options, this.dependencies.contextSrv.user?.orgId);
-
- const fromFetchStream = this.getFromFetchStream(options);
- const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
- const successStream = fromFetchStream.pipe(
- filter(response => response.ok === true),
- map(response => {
- const fetchSuccessResponse: SuccessResponse = response.data;
- return fetchSuccessResponse;
- }),
- tap(response => {
- if (options.method !== 'GET' && response?.message && options.showSuccessAlert !== false) {
- this.dependencies.appEvents.emit(AppEvents.alertSuccess, [response.message]);
- }
- })
- );
-
- return merge(successStream, failureStream)
- .pipe(
- catchError((err: ErrorResponse) => {
- // this setTimeout hack enables any caller catching this err to set isHandled to true
- setTimeout(() => this.requestErrorHandler(err), 50);
- return throwError(err);
- }),
- this.handleStreamCancellation(options, CancellationType.request)
- )
- .toPromise();
- }
-
- resolveCancelerIfExists(requestId: string) {
- this.inFlightRequests.next(requestId);
- }
-
- cancelAllInFlightRequests() {
- this.inFlightRequests.next(CANCEL_ALL_REQUESTS_REQUEST_ID);
- }
-
- async datasourceRequest(options: BackendSrvRequest): Promise {
- // A requestId is provided by the datasource as a unique identifier for a
- // particular query. Every observable below has a takeUntil that subscribes to this.inFlightRequests and
- // will cancel/unsubscribe that observable when a new datasourceRequest with the same requestId is made
- if (options.requestId) {
- this.inFlightRequests.next(options.requestId);
- }
-
- options = this.parseDataSourceRequestOptions(
- options,
- this.dependencies.contextSrv.user?.orgId,
- this.noBackendCache
- );
-
- const fromFetchStream = this.getFromFetchStream(options);
- const failureStream = fromFetchStream.pipe(this.toDataSourceRequestFailureStream(options));
- const successStream = fromFetchStream.pipe(
- filter(response => response.ok === true),
- map(response => {
- const fetchSuccessResponse: DataSourceSuccessResponse = { ...response };
- return fetchSuccessResponse;
- }),
- tap(res => {
- if (!options.silent) {
- this.dependencies.appEvents.emit(CoreEvents.dsRequestResponse, res);
- }
- })
- );
-
- return merge(successStream, failureStream)
- .pipe(
- catchError((err: ErrorResponse) => {
- // populate error obj on Internal Error
- if (typeof err.data === 'string' && err.status === 500) {
- err.data = {
- error: err.statusText,
- response: err.data,
- };
- }
-
- // for Prometheus
- if (err.data && !err.data.message && typeof err.data.error === 'string') {
- err.data.message = err.data.error;
- }
-
- if (!options.silent) {
- this.dependencies.appEvents.emit(CoreEvents.dsRequestError, err);
- }
-
- return throwError(err);
- }),
- this.handleStreamCancellation(options, CancellationType.dataSourceRequest)
- )
- .toPromise();
- }
-
loginPing() {
return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
}
@@ -263,315 +355,22 @@ export class BackendSrv implements BackendService {
getFolderByUid(uid: string) {
return this.get(`/api/folders/${uid}`);
}
+}
- saveDashboard(
- dashboard: DashboardDataDTO,
- { message = '', folderId, overwrite = false }: { message?: string; folderId?: number; overwrite?: boolean } = {}
+function isDataQuery(url: string): boolean {
+ if (
+ url.indexOf('api/datasources/proxy') !== -1 ||
+ url.indexOf('api/tsdb/query') !== -1 ||
+ url.indexOf('api/ds/query') !== -1
) {
- return this.post('/api/dashboards/db/', {
- dashboard,
- folderId,
- overwrite,
- message,
- });
+ return true;
}
- createFolder(payload: any) {
- return this.post('/api/folders', payload);
- }
+ return false;
+}
- 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: result.filter((res: any) => res.succeeded).length,
- alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length,
- };
- });
- }
-
- private async moveDashboard(uid: string, toFolder: FolderInfo) {
- const fullDash: DashboardDTO = await this.getDashboardByUid(uid);
-
- if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
- return { alreadyInFolder: true };
- }
-
- const clone = fullDash.dashboard;
- const options = {
- folderId: toFolder.id,
- overwrite: false,
- };
-
- try {
- await this.saveDashboard(clone, options);
- return { succeeded: true };
- } catch (err) {
- if (err.data?.status !== 'plugin-dashboard') {
- return { succeeded: false };
- }
-
- err.isHandled = true;
- options.overwrite = true;
-
- try {
- await this.saveDashboard(clone, options);
- return { succeeded: true };
- } catch (e) {
- return { succeeded: false };
- }
- }
- }
-
- private createTask(fn: (...args: any[]) => Promise, ignoreRejections: boolean, ...args: any[]) {
- return async (result: any) => {
- try {
- const res = await fn(...args);
- return Array.prototype.concat(result, [res]);
- } catch (err) {
- if (ignoreRejections) {
- return result;
- }
-
- throw err;
- }
- };
- }
-
- private executeInOrder(tasks: any[]) {
- return tasks.reduce((acc, task) => {
- return Promise.resolve(acc).then(task);
- }, []);
- }
-
- private parseRequestOptions = (options: BackendSrvRequest, orgId?: number): BackendSrvRequest => {
- options.retry = options.retry ?? 0;
- const requestIsLocal = !options.url.match(/^http/);
-
- if (requestIsLocal) {
- if (orgId) {
- options.headers = options.headers ?? {};
- options.headers['X-Grafana-Org-Id'] = orgId;
- }
-
- if (options.url.startsWith('/')) {
- options.url = options.url.substring(1);
- }
-
- if (options.url.endsWith('/')) {
- options.url = options.url.slice(0, -1);
- }
- }
-
- return options;
- };
-
- private parseDataSourceRequestOptions = (
- options: BackendSrvRequest,
- orgId?: number,
- noBackendCache?: boolean
- ): BackendSrvRequest => {
- options.retry = options.retry ?? 0;
- const requestIsLocal = !options.url.match(/^http/);
-
- if (requestIsLocal) {
- if (orgId) {
- options.headers = options.headers || {};
- options.headers['X-Grafana-Org-Id'] = orgId;
- }
-
- if (options.url.startsWith('/')) {
- options.url = options.url.substring(1);
- }
-
- if (options.headers?.Authorization) {
- options.headers['X-DS-Authorization'] = options.headers.Authorization;
- delete options.headers.Authorization;
- }
-
- if (noBackendCache) {
- options.headers['X-Grafana-NoCache'] = 'true';
- }
- }
-
- return options;
- };
-
- private getFromFetchStream = (options: BackendSrvRequest) => {
- const url = parseUrlFromOptions(options);
- const init = parseInitFromOptions(options);
- return this.dependencies.fromFetch(url, init).pipe(
- mergeMap(async response => {
- const { status, statusText, ok, headers, url, type, redirected } = response;
- const textData = await response.text(); // this could be just a string, prometheus requests for instance
- let data;
- try {
- data = JSON.parse(textData); // majority of the requests this will be something that can be parsed
- } catch {
- data = textData;
- }
- const fetchResponse: FetchResponse = {
- status,
- statusText,
- ok,
- data,
- headers,
- url,
- type,
- redirected,
- config: options,
- };
- return fetchResponse;
- }),
- share() // sharing this so we can split into success and failure and then merge back
- );
- };
-
- private toFailureStream = (options: BackendSrvRequest): MonoTypeOperatorFunction => inputStream =>
- inputStream.pipe(
- filter(response => response.ok === false),
- mergeMap(response => {
- const { status, statusText, data } = response;
- const fetchErrorResponse: ErrorResponse = { status, statusText, data };
- return throwError(fetchErrorResponse);
- }),
- retryWhen((attempts: Observable) =>
- attempts.pipe(
- mergeMap((error, i) => {
- const firstAttempt = i === 0 && options.retry === 0;
-
- if (error.status === 401 && this.dependencies.contextSrv.user.isSignedIn && firstAttempt) {
- return from(this.loginPing()).pipe(
- catchError(err => {
- if (err.status === 401) {
- this.dependencies.logout();
- return throwError(err);
- }
- return throwError(err);
- })
- );
- }
-
- return throwError(error);
- })
- )
- )
- );
-
- private toDataSourceRequestFailureStream = (
- options: BackendSrvRequest
- ): MonoTypeOperatorFunction => inputStream =>
- inputStream.pipe(
- filter(response => response.ok === false),
- mergeMap(response => {
- const { status, statusText, data } = response;
- const fetchErrorResponse: ErrorResponse = { status, statusText, data };
- return throwError(fetchErrorResponse);
- }),
- retryWhen((attempts: Observable) =>
- attempts.pipe(
- mergeMap((error, i) => {
- const requestIsLocal = !options.url.match(/^http/);
- const firstAttempt = i === 0 && options.retry === 0;
-
- // First retry, if loginPing returns 401 this retry sequence will abort with throwError and user is logged out
- if (requestIsLocal && firstAttempt && error.status === 401) {
- return from(this.loginPing()).pipe(
- catchError(err => {
- if (err.status === 401) {
- this.dependencies.logout();
- return throwError(err);
- }
- return throwError(err);
- })
- );
- }
-
- return throwError(error);
- })
- )
- )
- );
-
- private handleStreamCancellation = (
- options: BackendSrvRequest,
- resultType: CancellationType
- ): MonoTypeOperatorFunction => inputStream =>
- inputStream.pipe(
- takeUntil(
- this.inFlightRequests.pipe(
- filter(requestId => {
- let cancelRequest = false;
-
- if (options && options.requestId && options.requestId === requestId) {
- // when a new requestId is started it will be published to inFlightRequests
- // if a previous long running request that hasn't finished yet has the same requestId
- // we need to cancel that request
- cancelRequest = true;
- }
-
- if (requestId === CANCEL_ALL_REQUESTS_REQUEST_ID) {
- cancelRequest = true;
- }
-
- return cancelRequest;
- })
- )
- ),
- // when a request is cancelled by takeUntil it will complete without emitting anything so we use throwIfEmpty to identify this case
- // in throwIfEmpty we'll then throw an cancelled error and then we'll return the correct result in the catchError or rethrow
- throwIfEmpty(() => ({
- cancelled: true,
- })),
- catchError(err => {
- if (!err.cancelled) {
- return throwError(err);
- }
-
- if (resultType === CancellationType.dataSourceRequest) {
- return of({
- data: [],
- status: this.HTTP_REQUEST_CANCELED,
- statusText: 'Request was aborted',
- config: options,
- });
- }
-
- return of([]);
- })
- );
+function isLocalUrl(url: string) {
+ return !url.match(/^http/);
}
coreModule.factory('backendSrv', () => backendSrv);
diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts
index 764102ad3a7..6cd022c9cc3 100644
--- a/public/app/core/specs/backend_srv.test.ts
+++ b/public/app/core/specs/backend_srv.test.ts
@@ -3,10 +3,9 @@ import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AppEvents } from '@grafana/data';
-import { BackendSrv, getBackendSrv } from '../services/backend_srv';
+import { BackendSrv } from '../services/backend_srv';
import { Emitter } from '../utils/emitter';
import { ContextSrv, User } from '../services/context_srv';
-import { CoreEvents } from '../../types';
import { describe, expect } from '../../../test/lib/common';
const getTestContext = (overides?: object) => {
@@ -47,7 +46,6 @@ const getTestContext = (overides?: object) => {
} as any) as ContextSrv;
const logoutMock = jest.fn();
const parseRequestOptionsMock = jest.fn().mockImplementation(options => options);
- const parseDataSourceRequestOptionsMock = jest.fn().mockImplementation(options => options);
const backendSrv = new BackendSrv({
fromFetch: fromFetchMock,
@@ -57,7 +55,6 @@ const getTestContext = (overides?: object) => {
});
backendSrv['parseRequestOptions'] = parseRequestOptionsMock;
- backendSrv['parseDataSourceRequestOptions'] = parseDataSourceRequestOptionsMock;
const expectCallChain = (options: any) => {
expect(fromFetchMock).toHaveBeenCalledTimes(1);
@@ -65,13 +62,7 @@ const getTestContext = (overides?: object) => {
const expectRequestCallChain = (options: any) => {
expect(parseRequestOptionsMock).toHaveBeenCalledTimes(1);
- expect(parseRequestOptionsMock).toHaveBeenCalledWith(options, 1337);
- expectCallChain(options);
- };
-
- const expectDataSourceRequestCallChain = (options: any) => {
- expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledTimes(1);
- expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledWith(options, 1337, undefined);
+ expect(parseRequestOptionsMock).toHaveBeenCalledWith(options);
expectCallChain(options);
};
@@ -83,33 +74,12 @@ const getTestContext = (overides?: object) => {
textMock,
logoutMock,
parseRequestOptionsMock,
- parseDataSourceRequestOptionsMock,
expectRequestCallChain,
- expectDataSourceRequestCallChain,
};
};
describe('backendSrv', () => {
describe('parseRequestOptions', () => {
- it.each`
- retry | url | orgId | expected
- ${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }}
- ${1} | ${'http://localhost:3000/api/dashboard'} | ${1} | ${{ retry: 1, url: 'http://localhost:3000/api/dashboard' }}
- ${undefined} | ${'api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
- ${undefined} | ${'/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
- ${undefined} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }}
- ${1} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 1, url: 'api/dashboard' }}
- ${undefined} | ${'/api/dashboard/'} | ${1} | ${{ retry: 0, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }}
- ${1} | ${'/api/dashboard/'} | ${1} | ${{ retry: 1, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }}
- `(
- "when called with retry: '$retry', url: '$url' and orgId: '$orgId' then result should be '$expected'",
- ({ retry, url, orgId, expected }) => {
- expect(getBackendSrv()['parseRequestOptions']({ retry, url }, orgId)).toEqual(expected);
- }
- );
- });
-
- describe('parseDataSourceRequestOptions', () => {
it.each`
retry | url | headers | orgId | noBackendCache | expected
${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }}
@@ -125,11 +95,23 @@ describe('backendSrv', () => {
${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1 } }}
${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1, 'X-Grafana-NoCache': 'true' } }}
`(
- "when called with retry: '$retry', url: '$url', headers: '$headers', orgId: '$orgId' and noBackendCache: '$noBackendCache' then result should be '$expected'",
+ "when called with retry: '$retry', url: '$url' and orgId: '$orgId' then result should be '$expected'",
({ retry, url, headers, orgId, noBackendCache, expected }) => {
- expect(
- getBackendSrv()['parseDataSourceRequestOptions']({ retry, url, headers }, orgId, noBackendCache)
- ).toEqual(expected);
+ const srv = new BackendSrv({
+ contextSrv: {
+ user: {
+ orgId: orgId,
+ },
+ },
+ } as any);
+
+ if (noBackendCache) {
+ srv.withNoBackendCache(async () => {
+ expect(srv['parseRequestOptions']({ retry, url, headers })).toEqual(expected);
+ });
+ } else {
+ expect(srv['parseRequestOptions']({ retry, url, headers })).toEqual(expected);
+ }
}
);
});
@@ -142,6 +124,7 @@ describe('backendSrv', () => {
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: false });
+
expect(result).toEqual({ message: 'A message' });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false });
@@ -155,6 +138,7 @@ describe('backendSrv', () => {
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: true });
+
expect(result).toEqual({ message: 'A message' });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, ['A message']);
@@ -162,61 +146,6 @@ describe('backendSrv', () => {
});
});
- describe('when called with the same requestId twice', () => {
- it('then it should cancel the first call and the first call should be unsubscribed', async () => {
- const url = '/api/dashboard/';
- const { backendSrv, fromFetchMock } = getTestContext({ url });
- const unsubscribe = jest.fn();
- const slowData = { message: 'Slow Request' };
- const slowFetch = new Observable(subscriber => {
- subscriber.next({
- ok: true,
- status: 200,
- statusText: 'Ok',
- text: () => Promise.resolve(JSON.stringify(slowData)),
- headers: {
- map: {
- 'content-type': 'application/json',
- },
- },
- redirected: false,
- type: 'basic',
- url,
- });
- return unsubscribe;
- }).pipe(delay(10000));
- const fastData = { message: 'Fast Request' };
- const fastFetch = of({
- ok: true,
- status: 200,
- statusText: 'Ok',
- text: () => Promise.resolve(JSON.stringify(fastData)),
- headers: {
- map: {
- 'content-type': 'application/json',
- },
- },
- redirected: false,
- type: 'basic',
- url,
- });
- fromFetchMock.mockImplementationOnce(() => slowFetch);
- fromFetchMock.mockImplementation(() => fastFetch);
- const options = {
- url,
- method: 'GET',
- requestId: 'A',
- };
- const slowRequest = backendSrv.request(options);
- const fastResponse = await backendSrv.request(options);
- expect(fastResponse).toEqual({ message: 'Fast Request' });
-
- const result = await slowRequest;
- expect(result).toEqual([]);
- expect(unsubscribe).toHaveBeenCalledTimes(1);
- });
- });
-
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
it('then it should retry', async () => {
jest.useFakeTimers();
@@ -228,9 +157,11 @@ describe('backendSrv', () => {
data: { message: 'UnAuthorized' },
url,
});
+
backendSrv.loginPing = jest
.fn()
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
+
await backendSrv
.request({ url, method: 'GET', retry: 0 })
.catch(error => {
@@ -260,10 +191,12 @@ describe('backendSrv', () => {
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
+
backendSrv.loginPing = jest
.fn()
.mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } });
const url = '/api/dashboard/';
+
await backendSrv
.request({ url, method: 'GET', retry: 0 })
.catch(error => {
@@ -294,6 +227,7 @@ describe('backendSrv', () => {
data: { message: 'Unprocessable Entity' },
});
const url = '/api/dashboard/';
+
await backendSrv
.request({ url, method: 'GET' })
.catch(error => {
@@ -326,6 +260,7 @@ describe('backendSrv', () => {
data: { message: 'Not found' },
});
const url = '/api/dashboard/';
+
await backendSrv.request({ url, method: 'GET' }).catch(error => {
expect(error.status).toBe(404);
expect(error.statusText).toBe('Not found');
@@ -345,9 +280,10 @@ describe('backendSrv', () => {
describe('when making a successful call and silent is true', () => {
it('then it should not emit message', async () => {
const url = 'http://localhost:3000/api/some-mock';
- const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext({ url });
+ const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({ url });
const options = { url, method: 'GET', silent: true };
const result = await backendSrv.datasourceRequest(options);
+
expect(result).toEqual({
data: { test: 'hello world' },
ok: true,
@@ -358,16 +294,23 @@ describe('backendSrv', () => {
url,
config: options,
});
+
expect(appEventsMock.emit).not.toHaveBeenCalled();
- expectDataSourceRequestCallChain({ url, method: 'GET', silent: true });
+ expectRequestCallChain({ url, method: 'GET', silent: true });
});
});
describe('when making a successful call and silent is not defined', () => {
it('then it should not emit message', async () => {
const url = 'http://localhost:3000/api/some-mock';
- const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext({ url });
+ const { backendSrv, expectRequestCallChain } = getTestContext({ url });
const options = { url, method: 'GET' };
+
+ let inspectorPacket: any = null;
+ backendSrv.getInspectorStream().subscribe({
+ next: rsp => (inspectorPacket = rsp),
+ });
+
const result = await backendSrv.datasourceRequest(options);
const expectedResult = {
data: { test: 'hello world' },
@@ -381,9 +324,8 @@ describe('backendSrv', () => {
};
expect(result).toEqual(expectedResult);
- expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
- expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestResponse, expectedResult);
- expectDataSourceRequestCallChain({ url, method: 'GET' });
+ expect(inspectorPacket).toEqual(expectedResult);
+ expectRequestCallChain({ url, method: 'GET' });
});
});
@@ -405,6 +347,7 @@ describe('backendSrv', () => {
});
return unsubscribe;
}).pipe(delay(10000));
+
const fastData = { message: 'Fast Request' };
const fastFetch = of({
ok: true,
@@ -415,118 +358,119 @@ describe('backendSrv', () => {
type: 'basic',
url,
});
+
fromFetchMock.mockImplementationOnce(() => slowFetch);
fromFetchMock.mockImplementation(() => fastFetch);
+
const options = {
url,
method: 'GET',
requestId: 'A',
};
- const slowRequest = backendSrv.datasourceRequest(options);
- const fastResponse = await backendSrv.datasourceRequest(options);
- expect(fastResponse).toEqual({
- data: { message: 'Fast Request' },
- ok: true,
- redirected: false,
- status: 200,
- statusText: 'Ok',
- type: 'basic',
- url: '/api/dashboard/',
- config: options,
+
+ let slowError: any = null;
+ backendSrv.request(options).catch(err => {
+ slowError = err;
});
- const result = await slowRequest;
- expect(result).toEqual({
- data: [],
+ const fastResponse = await backendSrv.request(options);
+
+ expect(fastResponse).toEqual({
+ message: 'Fast Request',
+ });
+
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
+
+ expect(slowError).toEqual({
+ cancelled: true,
+ data: null,
status: -1,
statusText: 'Request was aborted',
config: options,
});
- expect(unsubscribe).toHaveBeenCalledTimes(1);
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
it('then it should retry', async () => {
- const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
+ const { backendSrv, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
+
backendSrv.loginPing = jest
.fn()
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
const url = '/api/dashboard/';
+
+ let inspectorPacket: any = null;
+ backendSrv.getInspectorStream().subscribe({
+ next: rsp => (inspectorPacket = rsp),
+ });
+
await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
expect(error.status).toBe(401);
expect(error.statusText).toBe('UnAuthorized');
expect(error.data).toEqual({ message: 'UnAuthorized' });
- expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
- expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
- data: { message: 'UnAuthorized' },
- status: 401,
- statusText: 'UnAuthorized',
- });
+ expect(inspectorPacket).toBe(error);
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).not.toHaveBeenCalled();
- expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
+ expectRequestCallChain({ url, method: 'GET', retry: 0 });
});
});
});
describe('when making an unsuccessful call and conditions for retry are favorable and retry throws', () => {
it('then it throw error', async () => {
- const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
+ const { backendSrv, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
});
+
+ const options = {
+ url: '/api/dashboard/',
+ method: 'GET',
+ retry: 0,
+ };
+
backendSrv.loginPing = jest
.fn()
.mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } });
- const url = '/api/dashboard/';
- await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
+
+ await backendSrv.datasourceRequest(options).catch(error => {
expect(error.status).toBe(403);
expect(error.statusText).toBe('Forbidden');
expect(error.data).toEqual({ message: 'Forbidden' });
- expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
- expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
- data: { message: 'Forbidden' },
- status: 403,
- statusText: 'Forbidden',
- });
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
expect(logoutMock).not.toHaveBeenCalled();
- expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
+ expectRequestCallChain(options);
});
});
});
describe('when making an Internal Error call', () => {
it('then it should throw cancelled error', async () => {
- const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
+ const { backendSrv, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 500,
statusText: 'Internal Server Error',
data: 'Internal Server Error',
});
- const url = '/api/dashboard/';
- await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => {
+
+ const options = {
+ url: '/api/dashboard/',
+ method: 'GET',
+ };
+
+ await backendSrv.datasourceRequest(options).catch(error => {
expect(error).toEqual({
status: 500,
statusText: 'Internal Server Error',
- data: {
- error: 'Internal Server Error',
- response: 'Internal Server Error',
- message: 'Internal Server Error',
- },
- });
- expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
- expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
- status: 500,
- statusText: 'Internal Server Error',
+ config: options,
data: {
error: 'Internal Server Error',
response: 'Internal Server Error',
@@ -534,40 +478,42 @@ describe('backendSrv', () => {
},
});
expect(logoutMock).not.toHaveBeenCalled();
- expectDataSourceRequestCallChain({ url, method: 'GET' });
+ expectRequestCallChain(options);
});
});
});
describe('when formatting prometheus error', () => {
it('then it should throw cancelled error', async () => {
- const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({
+ const { backendSrv, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 403,
statusText: 'Forbidden',
data: { error: 'Forbidden' },
});
- const url = '/api/dashboard/';
- await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => {
+ const options = {
+ url: '/api/dashboard/',
+ method: 'GET',
+ };
+
+ let inspectorPacket: any = null;
+ backendSrv.getInspectorStream().subscribe({
+ next: rsp => (inspectorPacket = rsp),
+ });
+
+ await backendSrv.datasourceRequest(options).catch(error => {
expect(error).toEqual({
status: 403,
statusText: 'Forbidden',
+ config: options,
data: {
error: 'Forbidden',
message: 'Forbidden',
},
});
- expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
- expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
- status: 403,
- statusText: 'Forbidden',
- data: {
- error: 'Forbidden',
- message: 'Forbidden',
- },
- });
+ expect(inspectorPacket).toEqual(error);
expect(logoutMock).not.toHaveBeenCalled();
- expectDataSourceRequestCallChain({ url, method: 'GET' });
+ expectRequestCallChain(options);
});
});
});
@@ -575,23 +521,7 @@ describe('backendSrv', () => {
describe('cancelAllInFlightRequests', () => {
describe('when called with 2 separate requests and then cancelAllInFlightRequests is called', () => {
- enum RequestType {
- request,
- dataSourceRequest,
- }
-
const url = '/api/dashboard/';
- const options = {
- url,
- method: 'GET',
- };
-
- const dataSourceRequestResult = {
- data: ([] as unknown[]) as any[],
- status: -1,
- statusText: 'Request was aborted',
- config: options,
- };
const getRequestObservable = (message: string, unsubscribe: any) =>
new Observable(subscriber => {
@@ -612,47 +542,37 @@ describe('backendSrv', () => {
return unsubscribe;
}).pipe(delay(10000));
- it.each`
- firstRequestType | secondRequestType | firstRequestResult | secondRequestResult
- ${RequestType.request} | ${RequestType.request} | ${[]} | ${[]}
- ${RequestType.dataSourceRequest} | ${RequestType.dataSourceRequest} | ${dataSourceRequestResult} | ${dataSourceRequestResult}
- ${RequestType.request} | ${RequestType.dataSourceRequest} | ${[]} | ${dataSourceRequestResult}
- ${RequestType.dataSourceRequest} | ${RequestType.request} | ${dataSourceRequestResult} | ${[]}
- `(
- 'then it both requests should be cancelled and unsubscribed',
- async ({ firstRequestType, secondRequestType, firstRequestResult, secondRequestResult }) => {
- const unsubscribe = jest.fn();
- const { backendSrv, fromFetchMock } = getTestContext({ url });
- const firstObservable = getRequestObservable('First', unsubscribe);
- const secondObservable = getRequestObservable('Second', unsubscribe);
+ it('then it both requests should be cancelled and unsubscribed', async () => {
+ const unsubscribe = jest.fn();
+ const { backendSrv, fromFetchMock } = getTestContext({ url });
+ const firstObservable = getRequestObservable('First', unsubscribe);
+ const secondObservable = getRequestObservable('Second', unsubscribe);
- fromFetchMock.mockImplementationOnce(() => firstObservable);
- fromFetchMock.mockImplementation(() => secondObservable);
+ fromFetchMock.mockImplementationOnce(() => firstObservable);
+ fromFetchMock.mockImplementation(() => secondObservable);
- const options = {
- url,
- method: 'GET',
- };
+ const options = {
+ url,
+ method: 'GET',
+ };
- const firstRequest =
- firstRequestType === RequestType.request
- ? backendSrv.request(options)
- : backendSrv.datasourceRequest(options);
+ const firstRequest = backendSrv.request(options);
+ const secondRequest = backendSrv.request(options);
- const secondRequest =
- secondRequestType === RequestType.request
- ? backendSrv.request(options)
- : backendSrv.datasourceRequest(options);
+ backendSrv.cancelAllInFlightRequests();
- backendSrv.cancelAllInFlightRequests();
+ let catchedError: any = null;
- const result = await Promise.all([firstRequest, secondRequest]);
-
- expect(result[0]).toEqual(firstRequestResult);
- expect(result[1]).toEqual(secondRequestResult);
- expect(unsubscribe).toHaveBeenCalledTimes(2);
+ try {
+ await Promise.all([firstRequest, secondRequest]);
+ } catch (err) {
+ catchedError = err;
}
- );
+
+ expect(catchedError.cancelled).toEqual(true);
+ expect(catchedError.statusText).toEqual('Request was aborted');
+ expect(unsubscribe).toHaveBeenCalledTimes(2);
+ });
});
});
});
diff --git a/public/app/features/admin/state/actions.ts b/public/app/features/admin/state/actions.ts
index d2891c951c5..e90e49b95ce 100644
--- a/public/app/features/admin/state/actions.ts
+++ b/public/app/features/admin/state/actions.ts
@@ -38,11 +38,12 @@ export function loadAdminUserPage(userId: number): ThunkResult {
dispatch(userAdminPageLoadedAction(true));
} catch (error) {
console.log(error);
- error.isHandled = true;
+
const userError = {
title: error.data.message,
body: error.data.error,
};
+
dispatch(userAdminPageFailedAction(userError));
}
};
diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
index dce4789f33c..f2309eb3ee5 100644
--- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
+++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
@@ -6,12 +6,12 @@ import { selectors } from '@grafana/e2e-selectors';
import { appEvents, contextSrv, coreModule } from 'app/core/core';
import { DashboardModel } from '../../state/DashboardModel';
import { getConfig } from 'app/core/config';
-import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from '../../services/DashboardSrv';
import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AppEvents, locationUtil, TimeZone } from '@grafana/data';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
+import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
export class SettingsCtrl {
dashboard: DashboardModel;
@@ -229,7 +229,7 @@ export class SettingsCtrl {
deleteDashboardConfirmed() {
promiseToDigest(this.$scope)(
- backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => {
+ deleteDashboard(this.dashboard.uid, false).then(() => {
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
this.$location.url('/');
})
diff --git a/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
index d52f96c4917..40121de191c 100644
--- a/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
+++ b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
@@ -8,6 +8,7 @@ import { backendSrv } from 'app/core/services/backend_srv';
import { ValidationSrv } from 'app/features/manage-dashboards';
import { ContextSrv } from 'app/core/services/context_srv';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
+import { createFolder } from 'app/features/manage-dashboards/state/actions';
export class FolderPickerCtrl {
initialTitle: string;
@@ -111,7 +112,7 @@ export class FolderPickerCtrl {
}
return promiseToDigest(this.$scope)(
- backendSrv.createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => {
+ createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => {
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
this.closeCreateFolder();
diff --git a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx
index a5df84432d3..f026f02bdc7 100644
--- a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx
+++ b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx
@@ -5,12 +5,13 @@ import { AppEvents, PanelEvents, DataFrame } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
-import { CoreEvents } from 'app/types';
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles';
import { supportsDataQuery } from '../PanelEditor/utils';
import { config } from '@grafana/runtime';
import { css } from 'emotion';
+import { Unsubscribable } from 'rxjs';
+import { backendSrv } from 'app/core/services/backend_srv';
interface DsQuery {
isLoading: boolean;
@@ -40,6 +41,7 @@ interface State {
export class QueryInspector extends PureComponent {
formattedJson: any;
clipboard: any;
+ subscription?: Unsubscribable;
constructor(props: Props) {
super(props);
@@ -56,8 +58,10 @@ export class QueryInspector extends PureComponent {
}
componentDidMount() {
- appEvents.on(CoreEvents.dsRequestResponse, this.onDataSourceResponse);
- appEvents.on(CoreEvents.dsRequestError, this.onRequestError);
+ this.subscription = backendSrv.getInspectorStream().subscribe({
+ next: response => this.onDataSourceResponse(response),
+ });
+
this.props.panel.events.on(PanelEvents.refresh, this.onPanelRefresh);
this.updateQueryList();
}
@@ -74,12 +78,16 @@ export class QueryInspector extends PureComponent {
updateQueryList() {
const { data } = this.props;
const executedQueries: ExecutedQueryInfo[] = [];
+
if (data?.length) {
let last: ExecutedQueryInfo | undefined = undefined;
+
data.forEach((frame, idx) => {
const query = frame.meta?.executedQueryString;
+
if (query) {
const refId = frame.refId || '?';
+
if (last?.refId === refId) {
last.frames++;
last.rows += frame.length;
@@ -95,6 +103,7 @@ export class QueryInspector extends PureComponent {
}
});
}
+
this.setState({ executedQueries });
}
@@ -105,23 +114,11 @@ export class QueryInspector extends PureComponent {
componentWillUnmount() {
const { panel } = this.props;
- appEvents.off(CoreEvents.dsRequestResponse, this.onDataSourceResponse);
- appEvents.on(CoreEvents.dsRequestError, this.onRequestError);
-
- panel.events.off(PanelEvents.refresh, this.onPanelRefresh);
- }
-
- handleMocking(response: any) {
- const { mockedResponse } = this.state;
- let mockedData;
- try {
- mockedData = JSON.parse(mockedResponse);
- } catch (err) {
- appEvents.emit(AppEvents.alertError, ['R: Failed to parse mocked response']);
- return;
+ if (this.subscription) {
+ this.subscription.unsubscribe();
}
- response.data = mockedData;
+ panel.events.off(PanelEvents.refresh, this.onPanelRefresh);
}
onPanelRefresh = () => {
@@ -134,13 +131,9 @@ export class QueryInspector extends PureComponent {
}));
};
- onRequestError = (err: any) => {
- this.onDataSourceResponse(err);
- };
-
- onDataSourceResponse = (response: any = {}) => {
- if (this.state.isMocking) {
- this.handleMocking(response);
+ onDataSourceResponse(response: any) {
+ // ignore silent requests
+ if (response.config?.silent) {
return;
}
@@ -186,7 +179,7 @@ export class QueryInspector extends PureComponent {
response: response,
},
}));
- };
+ }
setFormattedJson = (formattedJson: any) => {
this.formattedJson = formattedJson;
diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
index 5519d37a36d..b7cdb8340bc 100644
--- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
@@ -7,11 +7,11 @@ import { CoreEvents, StoreState } from 'app/types';
import appEvents from 'app/core/app_events';
import { updateLocation } from 'app/core/reducers/location';
import { DashboardModel } from 'app/features/dashboard/state';
-import { getBackendSrv } from 'app/core/services/backend_srv';
+import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
const folderId = options.folderId >= 0 ? options.folderId : dashboard.meta.folderId || saveModel.folderId;
- return await getBackendSrv().saveDashboard(saveModel, { ...options, folderId });
+ return await saveDashboardApiCall({ ...options, folderId, dashboard: saveModel });
};
export const useDashboardSave = (dashboard: DashboardModel) => {
diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts
index 3f0734cd675..ed3fad95641 100644
--- a/public/app/features/dashboard/services/DashboardSrv.ts
+++ b/public/app/features/dashboard/services/DashboardSrv.ts
@@ -4,8 +4,9 @@ import { DashboardModel } from '../state/DashboardModel';
import { removePanel } from '../utils/panel';
import { CoreEvents, DashboardMeta } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
-import { backendSrv, getBackendSrv } from 'app/core/services/backend_srv';
+import { backendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from '../../../core/utils/promiseToDigest';
+import { saveDashboard } from 'app/features/manage-dashboards/state/actions';
export class DashboardSrv {
dashboard: DashboardModel;
@@ -34,7 +35,8 @@ export class DashboardSrv {
saveJSONDashboard(json: string) {
const parsedJson = JSON.parse(json);
- return getBackendSrv().saveDashboard(parsedJson, {
+ return saveDashboard({
+ dashboard: parsedJson,
folderId: this.dashboard.meta.folderId || parsedJson.folderId,
});
}
diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts
index 1e03fee25f3..47b16a0bc97 100644
--- a/public/app/features/dashboard/state/runRequest.ts
+++ b/public/app/features/dashboard/state/runRequest.ts
@@ -115,13 +115,14 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
return state.panelData;
}),
// handle errors
- catchError(err =>
- of({
+ catchError(err => {
+ console.log('runRequest.catchError', err);
+ return of({
...state.panelData,
state: LoadingState.Error,
error: toDataQueryError(err),
- })
- ),
+ });
+ }),
tap(emitDataRequestEvent(datasource)),
// finalize is triggered when subscriber unsubscribes
// This makes sure any still running network requests are cancelled
diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts
index ad9a7106a17..6ca74f2a45c 100644
--- a/public/app/features/folders/state/actions.ts
+++ b/public/app/features/folders/state/actions.ts
@@ -33,7 +33,7 @@ export function saveFolder(folder: FolderState): ThunkResult {
export function deleteFolder(uid: string): ThunkResult {
return async dispatch => {
- await backendSrv.deleteFolder(uid, true);
+ await backendSrv.delete(`/api/folders/${uid}`);
dispatch(updateLocation({ path: `dashboards` }));
};
}
diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx
index 776dc91e137..02d4eef20cb 100644
--- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx
+++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx
@@ -3,7 +3,7 @@ import { dateTimeFormat } from '@grafana/data';
import { Legend, Form } from '@grafana/ui';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { ImportDashboardForm } from './ImportDashboardForm';
-import { clearLoadedDashboard, saveDashboard } from '../state/actions';
+import { clearLoadedDashboard, importDashboard } from '../state/actions';
import { DashboardInputs, DashboardSource, ImportDashboardDTO } from '../state/reducers';
import { StoreState } from 'app/types';
@@ -19,7 +19,7 @@ interface ConnectedProps {
interface DispatchProps {
clearLoadedDashboard: typeof clearLoadedDashboard;
- saveDashboard: typeof saveDashboard;
+ importDashboard: typeof importDashboard;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
@@ -34,7 +34,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent {
};
onSubmit = (form: ImportDashboardDTO) => {
- this.props.saveDashboard(form);
+ this.props.importDashboard(form);
};
onCancel = () => {
@@ -116,7 +116,7 @@ const mapStateToProps: MapStateToProps = (
const mapDispatchToProps: MapDispatchToProps = {
clearLoadedDashboard,
- saveDashboard,
+ importDashboard,
};
export const ImportDashboardOverview = connect(mapStateToProps, mapDispatchToProps)(ImportDashboardOverviewUnConnected);
diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts
index 51e1da3f8e6..af1adf13f48 100644
--- a/public/app/features/manage-dashboards/state/actions.ts
+++ b/public/app/features/manage-dashboards/state/actions.ts
@@ -1,5 +1,5 @@
import { AppEvents, DataSourceInstanceSettings, DataSourceSelectItem, locationUtil } from '@grafana/data';
-import { getBackendSrv } from '@grafana/runtime';
+import { getBackendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
import {
clearDashboard,
@@ -10,7 +10,7 @@ import {
ImportDashboardDTO,
} from './reducers';
import { updateLocation } from 'app/core/actions';
-import { ThunkResult } from 'app/types';
+import { ThunkResult, FolderInfo, DashboardDTO, DashboardDataDTO } from 'app/types';
import { appEvents } from '../../../core/core';
export function fetchGcomDashboard(id: string): ThunkResult {
@@ -66,7 +66,7 @@ export function clearLoadedDashboard(): ThunkResult {
};
}
-export function saveDashboard(importDashboardForm: ImportDashboardDTO): ThunkResult {
+export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkResult {
return async (dispatch, getState) => {
const dashboard = getState().importDashboard.dashboard;
const inputs = getState().importDashboard.inputs;
@@ -118,3 +118,123 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i
return { name: val.name, value: val.name, meta: val.meta };
});
};
+
+export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
+ const tasks = [];
+
+ for (const uid of dashboardUids) {
+ tasks.push(createTask(moveDashboard, true, uid, toFolder));
+ }
+
+ return executeInOrder(tasks).then((result: any) => {
+ return {
+ totalCount: result.length,
+ successCount: result.filter((res: any) => res.succeeded).length,
+ alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length,
+ };
+ });
+}
+
+async function moveDashboard(uid: string, toFolder: FolderInfo) {
+ const fullDash: DashboardDTO = await getBackendSrv().getDashboardByUid(uid);
+
+ if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
+ return { alreadyInFolder: true };
+ }
+
+ const options = {
+ dashboard: fullDash.dashboard,
+ folderId: toFolder.id,
+ overwrite: false,
+ };
+
+ try {
+ await saveDashboard(options);
+ return { succeeded: true };
+ } catch (err) {
+ if (err.data?.status !== 'plugin-dashboard') {
+ return { succeeded: false };
+ }
+
+ err.isHandled = true;
+ options.overwrite = true;
+
+ try {
+ await saveDashboard(options);
+ return { succeeded: true };
+ } catch (e) {
+ return { succeeded: false };
+ }
+ }
+}
+
+function createTask(fn: (...args: any[]) => Promise, ignoreRejections: boolean, ...args: any[]) {
+ return async (result: any) => {
+ try {
+ const res = await fn(...args);
+ return Array.prototype.concat(result, [res]);
+ } catch (err) {
+ if (ignoreRejections) {
+ return result;
+ }
+
+ throw err;
+ }
+ };
+}
+
+export function deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
+ const tasks = [];
+
+ for (const folderUid of folderUids) {
+ tasks.push(createTask(deleteFolder, true, folderUid, true));
+ }
+
+ for (const dashboardUid of dashboardUids) {
+ tasks.push(createTask(deleteDashboard, true, dashboardUid, true));
+ }
+
+ return executeInOrder(tasks);
+}
+
+export interface SaveDashboardOptions {
+ dashboard: DashboardDataDTO;
+ message?: string;
+ folderId?: number;
+ overwrite?: boolean;
+}
+
+export function saveDashboard(options: SaveDashboardOptions) {
+ return getBackendSrv().post('/api/dashboards/db/', {
+ dashboard: options.dashboard,
+ message: options.message ?? '',
+ overwrite: options.overwrite ?? false,
+ folderId: options.folderId,
+ });
+}
+
+function deleteFolder(uid: string, showSuccessAlert: boolean) {
+ return getBackendSrv().request({
+ method: 'DELETE',
+ url: `/api/folders/${uid}`,
+ showSuccessAlert: showSuccessAlert === true,
+ });
+}
+
+export function createFolder(payload: any) {
+ return getBackendSrv().post('/api/folders', payload);
+}
+
+export function deleteDashboard(uid: string, showSuccessAlert: boolean) {
+ return getBackendSrv().request({
+ method: 'DELETE',
+ url: `/api/dashboards/uid/${uid}`,
+ showSuccessAlert: showSuccessAlert === true,
+ });
+}
+
+function executeInOrder(tasks: any[]) {
+ return tasks.reduce((acc, task) => {
+ return Promise.resolve(acc).then(task);
+ }, []);
+}
diff --git a/public/app/features/search/components/ConfirmDeleteModal.tsx b/public/app/features/search/components/ConfirmDeleteModal.tsx
index a55d5978afc..dec98283059 100644
--- a/public/app/features/search/components/ConfirmDeleteModal.tsx
+++ b/public/app/features/search/components/ConfirmDeleteModal.tsx
@@ -3,9 +3,9 @@ import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime';
-import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSection, OnDeleteItems } from '../types';
import { getCheckedUids } from '../utils';
+import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions';
interface Props {
onDeleteItems: OnDeleteItems;
@@ -38,7 +38,7 @@ export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen,
}
const deleteItems = () => {
- backendSrv.deleteFoldersAndDashboards(folders, dashboards).then(() => {
+ deleteFoldersAndDashboards(folders, dashboards).then(() => {
onDismiss();
// Redirect to /dashboard in case folder was deleted from f/:folder.uid
getLocationSrv().update({ path: '/dashboards' });
diff --git a/public/app/features/search/components/MoveToFolderModal.tsx b/public/app/features/search/components/MoveToFolderModal.tsx
index ef88bc11e5e..5f929729647 100644
--- a/public/app/features/search/components/MoveToFolderModal.tsx
+++ b/public/app/features/search/components/MoveToFolderModal.tsx
@@ -5,9 +5,9 @@ import { AppEvents, GrafanaTheme } from '@grafana/data';
import { FolderInfo } from 'app/types';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import appEvents from 'app/core/app_events';
-import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSection, OnMoveItems } from '../types';
import { getCheckedDashboards } from '../utils';
+import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
interface Props {
onMoveItems: OnMoveItems;
@@ -26,7 +26,7 @@ export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onD
if (folder && selectedDashboards.length) {
const folderTitle = folder.title ?? 'General';
- backendSrv.moveDashboards(selectedDashboards.map(d => d.uid) as string[], folder).then((result: any) => {
+ moveDashboards(selectedDashboards.map(d => d.uid) as string[], folder).then((result: any) => {
if (result.successCount > 0) {
const ending = result.successCount === 1 ? '' : 's';
const header = `Dashboard${ending} Moved`;
diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts
index 9aac9ebac40..701a2a8e8ef 100644
--- a/public/app/plugins/datasource/jaeger/datasource.ts
+++ b/public/app/plugins/datasource/jaeger/datasource.ts
@@ -9,12 +9,11 @@ import {
DataQuery,
FieldType,
} from '@grafana/data';
-import { getBackendSrv } from '@grafana/runtime';
+import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime';
import { Observable, from, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
-import { DatasourceRequestOptions } from 'app/core/services/backend_srv';
import { serializeParams } from 'app/core/utils/fetch';
export type JaegerQuery = {
@@ -87,7 +86,7 @@ export class JaegerDatasource extends DataSourceApi {
return query.query;
}
- private _request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable> {
+ private _request(apiUrl: string, data?: any, options?: Partial): Observable> {
// Hack for proxying metadata requests
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
const params = data ? serializeParams(data) : '';
diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts
index efe87ff8537..dd006d448e1 100644
--- a/public/app/plugins/datasource/loki/datasource.ts
+++ b/public/app/plugins/datasource/loki/datasource.ts
@@ -5,9 +5,8 @@ import { map, filter, catchError, switchMap } from 'rxjs/operators';
// Services & Utils
import { DataFrame, dateMath, FieldCache, QueryResultMeta } from '@grafana/data';
-import { getBackendSrv } from '@grafana/runtime';
+import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime';
import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query';
-import { DatasourceRequestOptions } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
import { lokiResultsToTableModel, processRangeQueryResponse, lokiStreamResultToDataFrame } from './result_transformer';
@@ -72,7 +71,7 @@ export class LokiDatasource extends DataSourceApi {
this.maxLines = parseInt(settingsData.maxLines ?? '0', 10) || DEFAULT_MAX_LINES;
}
- _request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable> {
+ _request(apiUrl: string, data?: any, options?: Partial): Observable> {
const baseUrl = this.instanceSettings.url;
const params = data ? serializeParams(data) : '';
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 196caee7361..e7463f6c3e1 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -1,7 +1,6 @@
// Libraries
import cloneDeep from 'lodash/cloneDeep';
import defaults from 'lodash/defaults';
-import $ from 'jquery';
// Services & Utils
import kbn from 'app/core/utils/kbn';
import {
@@ -130,7 +129,6 @@ export class PrometheusDatasource extends DataSourceApi
}
} else {
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
- options.transformRequest = (data: any) => $.param(data);
options.data = data;
}
diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts
index b9f43b2b40e..53c3a2ba04f 100644
--- a/public/app/plugins/datasource/testdata/datasource.ts
+++ b/public/app/plugins/datasource/testdata/datasource.ts
@@ -17,6 +17,7 @@ import { Scenario, TestDataQuery } from './types';
import { getBackendSrv, toDataQueryError } from '@grafana/runtime';
import { queryMetricTree } from './metricTree';
import { from, merge, Observable, of } from 'rxjs';
+import { map } from 'rxjs/operators';
import { runStream } from './runStreams';
import templateSrv from 'app/features/templating/template_srv';
import { getSearchFilterScopedVar } from 'app/features/variables/utils';
@@ -55,8 +56,8 @@ export class TestDataDataSource extends DataSourceApi {
}
if (queries.length) {
- const req: Promise = getBackendSrv()
- .datasourceRequest({
+ const stream = getBackendSrv()
+ .fetch({
method: 'POST',
url: '/api/tsdb/query',
data: {
@@ -64,12 +65,10 @@ export class TestDataDataSource extends DataSourceApi {
to: options.range.to.valueOf().toString(),
queries: queries,
},
- // This sets up a cancel token
- requestId: options.requestId,
})
- .then((res: any) => this.processQueryResult(queries, res));
+ .pipe(map(res => this.processQueryResult(queries, res)));
- streams.push(from(req));
+ streams.push(stream);
}
return merge(...streams);
diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts
index 44f0baf657f..c2b5020731d 100644
--- a/public/app/plugins/datasource/zipkin/datasource.ts
+++ b/public/app/plugins/datasource/zipkin/datasource.ts
@@ -8,9 +8,8 @@ import {
FieldType,
} from '@grafana/data';
import { from, Observable, of } from 'rxjs';
-import { DatasourceRequestOptions } from '../../../core/services/backend_srv';
import { serializeParams } from '../../../core/utils/fetch';
-import { getBackendSrv } from '@grafana/runtime';
+import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime';
import { map } from 'rxjs/operators';
import { apiPrefix } from './constants';
import { ZipkinSpan } from './types';
@@ -48,7 +47,7 @@ export class ZipkinDatasource extends DataSourceApi {
return query.query;
}
- private request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<{ data: T }> {
+ private request(apiUrl: string, data?: any, options?: Partial): Observable<{ data: T }> {
// Hack for proxying metadata requests
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
const params = data ? serializeParams(data) : '';