Commit 81e955e6 by Torkel Ödegaard Committed by GitHub

BackendSrv: Cancellable requests & Observable all the way (#25746)

* BackendSrv: Observable all the way POC

* starting to unify code paths

* tests pass

* Unified error handling

* Single request path and error handling

* Fixed ts issue

* another ts issu

* Added back old requestId cancellation

* Slow progress trying to grasp the full picture of cancellation

* Updates

* refactoring

* Remove a bunch of stuff from backendSrv

* Removed another function

* Do not show error alerts for data queries

* Muu

* Updated comment

* fixed ts issue

* unify request options type

* Made query inspector subscribe to backendSrv stream instead of legacy app events

* Add back support for err.isHandled to limit scope

* never show success alerts

* Updated tests

* Fixing tests

* Minor weak

* Improved logic for the showErrorAlert and showSuccessAlert boolean flags, now they work more logically

* Fix issue
parent ba4a8256
...@@ -18,6 +18,7 @@ The request function can be used to perform a remote call by specifying a [Backe ...@@ -18,6 +18,7 @@ The request function can be used to perform a remote call by specifying a [Backe
```typescript ```typescript
export interface BackendSrv export interface BackendSrv
``` ```
<b>Import</b> <b>Import</b>
```typescript ```typescript
...@@ -26,12 +27,14 @@ import { BackendSrv } from '@grafana/runtime'; ...@@ -26,12 +27,14 @@ import { BackendSrv } from '@grafana/runtime';
## Remarks ## 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
<b>Methods</b> <b>Methods</b>
| Method | Description | | 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 <code>option.silent</code> when initializing the request. | | [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 <code>option.silent</code> when initializing the request. |
| [delete(url)](#delete-method) | | | [delete(url)](#delete-method) | |
| [get(url, params, requestId)](#get-method) | | | [get(url, params, requestId)](#get-method) | |
...@@ -49,10 +52,11 @@ Special function used to communicate with datasources that will emit core events ...@@ -49,10 +52,11 @@ Special function used to communicate with datasources that will emit core events
```typescript ```typescript
datasourceRequest(options: BackendSrvRequest): Promise<any>; datasourceRequest(options: BackendSrvRequest): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------------------ | ----------- |
| options | <code>BackendSrvRequest</code> | | | options | <code>BackendSrvRequest</code> | |
<b>Returns:</b> <b>Returns:</b>
...@@ -66,10 +70,11 @@ datasourceRequest(options: BackendSrvRequest): Promise<any>; ...@@ -66,10 +70,11 @@ datasourceRequest(options: BackendSrvRequest): Promise<any>;
```typescript ```typescript
delete(url: string): Promise<any>; delete(url: string): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------- | ----------- |
| url | <code>string</code> | | | url | <code>string</code> | |
<b>Returns:</b> <b>Returns:</b>
...@@ -83,10 +88,11 @@ delete(url: string): Promise<any>; ...@@ -83,10 +88,11 @@ delete(url: string): Promise<any>;
```typescript ```typescript
get(url: string, params?: any, requestId?: string): Promise<any>; get(url: string, params?: any, requestId?: string): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------- | ----------- |
| url | <code>string</code> | | | url | <code>string</code> | |
| params | <code>any</code> | | | params | <code>any</code> | |
| requestId | <code>string</code> | | | requestId | <code>string</code> | |
...@@ -102,10 +108,11 @@ get(url: string, params?: any, requestId?: string): Promise<any>; ...@@ -102,10 +108,11 @@ get(url: string, params?: any, requestId?: string): Promise<any>;
```typescript ```typescript
patch(url: string, data?: any): Promise<any>; patch(url: string, data?: any): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------- | ----------- |
| url | <code>string</code> | | | url | <code>string</code> | |
| data | <code>any</code> | | | data | <code>any</code> | |
...@@ -120,10 +127,11 @@ patch(url: string, data?: any): Promise<any>; ...@@ -120,10 +127,11 @@ patch(url: string, data?: any): Promise<any>;
```typescript ```typescript
post(url: string, data?: any): Promise<any>; post(url: string, data?: any): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------- | ----------- |
| url | <code>string</code> | | | url | <code>string</code> | |
| data | <code>any</code> | | | data | <code>any</code> | |
...@@ -138,10 +146,11 @@ post(url: string, data?: any): Promise<any>; ...@@ -138,10 +146,11 @@ post(url: string, data?: any): Promise<any>;
```typescript ```typescript
put(url: string, data?: any): Promise<any>; put(url: string, data?: any): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------- | ----------- |
| url | <code>string</code> | | | url | <code>string</code> | |
| data | <code>any</code> | | | data | <code>any</code> | |
...@@ -156,13 +165,13 @@ put(url: string, data?: any): Promise<any>; ...@@ -156,13 +165,13 @@ put(url: string, data?: any): Promise<any>;
```typescript ```typescript
request(options: BackendSrvRequest): Promise<any>; request(options: BackendSrvRequest): Promise<any>;
``` ```
<b>Parameters</b> <b>Parameters</b>
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------------------------------ | ----------- |
| options | <code>BackendSrvRequest</code> | | | options | <code>BackendSrvRequest</code> | |
<b>Returns:</b> <b>Returns:</b>
`Promise<any>` `Promise<any>`
import { Observable } from 'rxjs';
/** /**
* Used to initiate a remote call via the {@link BackendSrv} * Used to initiate a remote call via the {@link BackendSrv}
* *
* @public * @public
*/ */
export type BackendSrvRequest = { export type BackendSrvRequest = {
/**
* Request URL
*/
url: string; url: string;
/** /**
* Number of times to retry the remote call if it fails. * Number of times to retry the remote call if it fails.
*/ */
...@@ -15,7 +21,7 @@ export type BackendSrvRequest = { ...@@ -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} * Please have a look at {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
* for supported headers. * for supported headers.
*/ */
headers?: any; headers?: Record<string, any>;
/** /**
* HTTP verb to perform in the remote call GET, POST, PUT etc. * HTTP verb to perform in the remote call GET, POST, PUT etc.
...@@ -23,22 +29,88 @@ export type BackendSrvRequest = { ...@@ -23,22 +29,88 @@ export type BackendSrvRequest = {
method?: string; method?: string;
/** /**
* If set to true an alert with the response message will be displayed * Set to false an success application alert box will not be shown for successful PUT, DELETE, POST requests
* upon successful remote call
*/ */
showSuccessAlert?: boolean; 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 * 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 * of this is when a datasource plugin triggers a query. If the request id already
* exist the backendSrv will try to cancel and replace the previous call with the * exist the backendSrv will try to cancel and replace the previous call with the
* new one. * new one.
*/ */
requestId?: string; 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<string, any>;
/**
* 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<T = any> {
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<T extends FetchErrorDataProps = any> {
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, * 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} * a datasource etc. The BackendSrv is using the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
* under the hood to handle all the communication. * under the hood to handle all the communication.
...@@ -48,9 +120,8 @@ export type BackendSrvRequest = { ...@@ -48,9 +120,8 @@ export type BackendSrvRequest = {
* use default values executing the request. * use default values executing the request.
* *
* @remarks * @remarks
* By default Grafana will display an error message alert if the remote call fails. If you want * By default, Grafana displays an error message alert if the remote call fails. To prevent this from
* to prevent this from happending you need to catch the error thrown by the BackendSrv and * happening `showErrorAlert = true` on the options object.
* set the `isHandled = true` on the incoming error.
* *
* @public * @public
*/ */
...@@ -60,15 +131,26 @@ export interface BackendSrv { ...@@ -60,15 +131,26 @@ export interface BackendSrv {
post(url: string, data?: any): Promise<any>; post(url: string, data?: any): Promise<any>;
patch(url: string, data?: any): Promise<any>; patch(url: string, data?: any): Promise<any>;
put(url: string, data?: any): Promise<any>; put(url: string, data?: any): Promise<any>;
/**
* @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<any>; request(options: BackendSrvRequest): Promise<any>;
/** /**
* @deprecated Use the fetch function instead
* Special function used to communicate with datasources that will emit core * Special function used to communicate with datasources that will emit core
* events that the Grafana QueryInspector and QueryEditor is listening for to be able * 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` * to display datasource query information. Can be skipped by adding `option.silent`
* when initializing the request. * when initializing the request.
*/ */
datasourceRequest(options: BackendSrvRequest): Promise<any>; datasourceRequest(options: BackendSrvRequest): Promise<any>;
/**
* Observable http request interface
*/
fetch<T>(options: BackendSrvRequest): Observable<FetchResponse<T>>;
} }
let singletonInstance: BackendSrv; let singletonInstance: BackendSrv;
......
...@@ -171,12 +171,11 @@ export class DataSourceWithBackend< ...@@ -171,12 +171,11 @@ export class DataSourceWithBackend<
*/ */
async callHealthCheck(): Promise<HealthCheckResult> { async callHealthCheck(): Promise<HealthCheckResult> {
return getBackendSrv() return getBackendSrv()
.get(`/api/datasources/${this.id}/health`) .request({ method: 'GET', url: `/api/datasources/${this.id}/health`, showErrorAlert: false })
.then(v => { .then(v => {
return v as HealthCheckResult; return v as HealthCheckResult;
}) })
.catch(err => { .catch(err => {
err.isHandled = true; // Avoid extra popup warning
return err.data as HealthCheckResult; return err.data as HealthCheckResult;
}); });
} }
......
...@@ -38,11 +38,12 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> { ...@@ -38,11 +38,12 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> {
dispatch(userAdminPageLoadedAction(true)); dispatch(userAdminPageLoadedAction(true));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
error.isHandled = true;
const userError = { const userError = {
title: error.data.message, title: error.data.message,
body: error.data.error, body: error.data.error,
}; };
dispatch(userAdminPageFailedAction(userError)); dispatch(userAdminPageFailedAction(userError));
} }
}; };
......
...@@ -6,12 +6,12 @@ import { selectors } from '@grafana/e2e-selectors'; ...@@ -6,12 +6,12 @@ import { selectors } from '@grafana/e2e-selectors';
import { appEvents, contextSrv, coreModule } from 'app/core/core'; import { appEvents, contextSrv, coreModule } from 'app/core/core';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { getConfig } from 'app/core/config'; import { getConfig } from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSrv } from '../../services/DashboardSrv'; import { DashboardSrv } from '../../services/DashboardSrv';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AppEvents, locationUtil, TimeZone } from '@grafana/data'; import { AppEvents, locationUtil, TimeZone } from '@grafana/data';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
export class SettingsCtrl { export class SettingsCtrl {
dashboard: DashboardModel; dashboard: DashboardModel;
...@@ -229,7 +229,7 @@ export class SettingsCtrl { ...@@ -229,7 +229,7 @@ export class SettingsCtrl {
deleteDashboardConfirmed() { deleteDashboardConfirmed() {
promiseToDigest(this.$scope)( 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']); appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
this.$location.url('/'); this.$location.url('/');
}) })
......
...@@ -8,6 +8,7 @@ import { backendSrv } from 'app/core/services/backend_srv'; ...@@ -8,6 +8,7 @@ import { backendSrv } from 'app/core/services/backend_srv';
import { ValidationSrv } from 'app/features/manage-dashboards'; import { ValidationSrv } from 'app/features/manage-dashboards';
import { ContextSrv } from 'app/core/services/context_srv'; import { ContextSrv } from 'app/core/services/context_srv';
import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
import { createFolder } from 'app/features/manage-dashboards/state/actions';
export class FolderPickerCtrl { export class FolderPickerCtrl {
initialTitle: string; initialTitle: string;
...@@ -111,7 +112,7 @@ export class FolderPickerCtrl { ...@@ -111,7 +112,7 @@ export class FolderPickerCtrl {
} }
return promiseToDigest(this.$scope)( 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']); appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
this.closeCreateFolder(); this.closeCreateFolder();
......
...@@ -5,12 +5,13 @@ import { AppEvents, PanelEvents, DataFrame } from '@grafana/data'; ...@@ -5,12 +5,13 @@ import { AppEvents, PanelEvents, DataFrame } from '@grafana/data';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { CoreEvents } from 'app/types';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles'; import { getPanelInspectorStyles } from './styles';
import { supportsDataQuery } from '../PanelEditor/utils'; import { supportsDataQuery } from '../PanelEditor/utils';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { css } from 'emotion'; import { css } from 'emotion';
import { Unsubscribable } from 'rxjs';
import { backendSrv } from 'app/core/services/backend_srv';
interface DsQuery { interface DsQuery {
isLoading: boolean; isLoading: boolean;
...@@ -40,6 +41,7 @@ interface State { ...@@ -40,6 +41,7 @@ interface State {
export class QueryInspector extends PureComponent<Props, State> { export class QueryInspector extends PureComponent<Props, State> {
formattedJson: any; formattedJson: any;
clipboard: any; clipboard: any;
subscription?: Unsubscribable;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
...@@ -56,8 +58,10 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -56,8 +58,10 @@ export class QueryInspector extends PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
appEvents.on(CoreEvents.dsRequestResponse, this.onDataSourceResponse); this.subscription = backendSrv.getInspectorStream().subscribe({
appEvents.on(CoreEvents.dsRequestError, this.onRequestError); next: response => this.onDataSourceResponse(response),
});
this.props.panel.events.on(PanelEvents.refresh, this.onPanelRefresh); this.props.panel.events.on(PanelEvents.refresh, this.onPanelRefresh);
this.updateQueryList(); this.updateQueryList();
} }
...@@ -74,12 +78,16 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -74,12 +78,16 @@ export class QueryInspector extends PureComponent<Props, State> {
updateQueryList() { updateQueryList() {
const { data } = this.props; const { data } = this.props;
const executedQueries: ExecutedQueryInfo[] = []; const executedQueries: ExecutedQueryInfo[] = [];
if (data?.length) { if (data?.length) {
let last: ExecutedQueryInfo | undefined = undefined; let last: ExecutedQueryInfo | undefined = undefined;
data.forEach((frame, idx) => { data.forEach((frame, idx) => {
const query = frame.meta?.executedQueryString; const query = frame.meta?.executedQueryString;
if (query) { if (query) {
const refId = frame.refId || '?'; const refId = frame.refId || '?';
if (last?.refId === refId) { if (last?.refId === refId) {
last.frames++; last.frames++;
last.rows += frame.length; last.rows += frame.length;
...@@ -95,6 +103,7 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -95,6 +103,7 @@ export class QueryInspector extends PureComponent<Props, State> {
} }
}); });
} }
this.setState({ executedQueries }); this.setState({ executedQueries });
} }
...@@ -105,23 +114,11 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -105,23 +114,11 @@ export class QueryInspector extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
const { panel } = this.props; const { panel } = this.props;
appEvents.off(CoreEvents.dsRequestResponse, this.onDataSourceResponse); if (this.subscription) {
appEvents.on(CoreEvents.dsRequestError, this.onRequestError); this.subscription.unsubscribe();
panel.events.off(PanelEvents.refresh, this.onPanelRefresh);
} }
handleMocking(response: any) { panel.events.off(PanelEvents.refresh, this.onPanelRefresh);
const { mockedResponse } = this.state;
let mockedData;
try {
mockedData = JSON.parse(mockedResponse);
} catch (err) {
appEvents.emit(AppEvents.alertError, ['R: Failed to parse mocked response']);
return;
}
response.data = mockedData;
} }
onPanelRefresh = () => { onPanelRefresh = () => {
...@@ -134,13 +131,9 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -134,13 +131,9 @@ export class QueryInspector extends PureComponent<Props, State> {
})); }));
}; };
onRequestError = (err: any) => { onDataSourceResponse(response: any) {
this.onDataSourceResponse(err); // ignore silent requests
}; if (response.config?.silent) {
onDataSourceResponse = (response: any = {}) => {
if (this.state.isMocking) {
this.handleMocking(response);
return; return;
} }
...@@ -186,7 +179,7 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -186,7 +179,7 @@ export class QueryInspector extends PureComponent<Props, State> {
response: response, response: response,
}, },
})); }));
}; }
setFormattedJson = (formattedJson: any) => { setFormattedJson = (formattedJson: any) => {
this.formattedJson = formattedJson; this.formattedJson = formattedJson;
......
...@@ -7,11 +7,11 @@ import { CoreEvents, StoreState } from 'app/types'; ...@@ -7,11 +7,11 @@ import { CoreEvents, StoreState } from 'app/types';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { updateLocation } from 'app/core/reducers/location'; import { updateLocation } from 'app/core/reducers/location';
import { DashboardModel } from 'app/features/dashboard/state'; 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 saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
const folderId = options.folderId >= 0 ? options.folderId : dashboard.meta.folderId || saveModel.folderId; 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) => { export const useDashboardSave = (dashboard: DashboardModel) => {
......
...@@ -4,8 +4,9 @@ import { DashboardModel } from '../state/DashboardModel'; ...@@ -4,8 +4,9 @@ import { DashboardModel } from '../state/DashboardModel';
import { removePanel } from '../utils/panel'; import { removePanel } from '../utils/panel';
import { CoreEvents, DashboardMeta } from 'app/types'; import { CoreEvents, DashboardMeta } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; 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 { promiseToDigest } from '../../../core/utils/promiseToDigest';
import { saveDashboard } from 'app/features/manage-dashboards/state/actions';
export class DashboardSrv { export class DashboardSrv {
dashboard: DashboardModel; dashboard: DashboardModel;
...@@ -34,7 +35,8 @@ export class DashboardSrv { ...@@ -34,7 +35,8 @@ export class DashboardSrv {
saveJSONDashboard(json: string) { saveJSONDashboard(json: string) {
const parsedJson = JSON.parse(json); const parsedJson = JSON.parse(json);
return getBackendSrv().saveDashboard(parsedJson, { return saveDashboard({
dashboard: parsedJson,
folderId: this.dashboard.meta.folderId || parsedJson.folderId, folderId: this.dashboard.meta.folderId || parsedJson.folderId,
}); });
} }
......
...@@ -115,13 +115,14 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) ...@@ -115,13 +115,14 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
return state.panelData; return state.panelData;
}), }),
// handle errors // handle errors
catchError(err => catchError(err => {
of({ console.log('runRequest.catchError', err);
return of({
...state.panelData, ...state.panelData,
state: LoadingState.Error, state: LoadingState.Error,
error: toDataQueryError(err), error: toDataQueryError(err),
}) });
), }),
tap(emitDataRequestEvent(datasource)), tap(emitDataRequestEvent(datasource)),
// finalize is triggered when subscriber unsubscribes // finalize is triggered when subscriber unsubscribes
// This makes sure any still running network requests are cancelled // This makes sure any still running network requests are cancelled
......
...@@ -33,7 +33,7 @@ export function saveFolder(folder: FolderState): ThunkResult<void> { ...@@ -33,7 +33,7 @@ export function saveFolder(folder: FolderState): ThunkResult<void> {
export function deleteFolder(uid: string): ThunkResult<void> { export function deleteFolder(uid: string): ThunkResult<void> {
return async dispatch => { return async dispatch => {
await backendSrv.deleteFolder(uid, true); await backendSrv.delete(`/api/folders/${uid}`);
dispatch(updateLocation({ path: `dashboards` })); dispatch(updateLocation({ path: `dashboards` }));
}; };
} }
......
...@@ -3,7 +3,7 @@ import { dateTimeFormat } from '@grafana/data'; ...@@ -3,7 +3,7 @@ import { dateTimeFormat } from '@grafana/data';
import { Legend, Form } from '@grafana/ui'; import { Legend, Form } from '@grafana/ui';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { ImportDashboardForm } from './ImportDashboardForm'; import { ImportDashboardForm } from './ImportDashboardForm';
import { clearLoadedDashboard, saveDashboard } from '../state/actions'; import { clearLoadedDashboard, importDashboard } from '../state/actions';
import { DashboardInputs, DashboardSource, ImportDashboardDTO } from '../state/reducers'; import { DashboardInputs, DashboardSource, ImportDashboardDTO } from '../state/reducers';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
...@@ -19,7 +19,7 @@ interface ConnectedProps { ...@@ -19,7 +19,7 @@ interface ConnectedProps {
interface DispatchProps { interface DispatchProps {
clearLoadedDashboard: typeof clearLoadedDashboard; clearLoadedDashboard: typeof clearLoadedDashboard;
saveDashboard: typeof saveDashboard; importDashboard: typeof importDashboard;
} }
type Props = OwnProps & ConnectedProps & DispatchProps; type Props = OwnProps & ConnectedProps & DispatchProps;
...@@ -34,7 +34,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> { ...@@ -34,7 +34,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
}; };
onSubmit = (form: ImportDashboardDTO) => { onSubmit = (form: ImportDashboardDTO) => {
this.props.saveDashboard(form); this.props.importDashboard(form);
}; };
onCancel = () => { onCancel = () => {
...@@ -116,7 +116,7 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = ( ...@@ -116,7 +116,7 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
clearLoadedDashboard, clearLoadedDashboard,
saveDashboard, importDashboard,
}; };
export const ImportDashboardOverview = connect(mapStateToProps, mapDispatchToProps)(ImportDashboardOverviewUnConnected); export const ImportDashboardOverview = connect(mapStateToProps, mapDispatchToProps)(ImportDashboardOverviewUnConnected);
......
import { AppEvents, DataSourceInstanceSettings, DataSourceSelectItem, locationUtil } from '@grafana/data'; 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 config from 'app/core/config';
import { import {
clearDashboard, clearDashboard,
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
ImportDashboardDTO, ImportDashboardDTO,
} from './reducers'; } from './reducers';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { ThunkResult } from 'app/types'; import { ThunkResult, FolderInfo, DashboardDTO, DashboardDataDTO } from 'app/types';
import { appEvents } from '../../../core/core'; import { appEvents } from '../../../core/core';
export function fetchGcomDashboard(id: string): ThunkResult<void> { export function fetchGcomDashboard(id: string): ThunkResult<void> {
...@@ -66,7 +66,7 @@ export function clearLoadedDashboard(): ThunkResult<void> { ...@@ -66,7 +66,7 @@ export function clearLoadedDashboard(): ThunkResult<void> {
}; };
} }
export function saveDashboard(importDashboardForm: ImportDashboardDTO): ThunkResult<void> { export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const dashboard = getState().importDashboard.dashboard; const dashboard = getState().importDashboard.dashboard;
const inputs = getState().importDashboard.inputs; const inputs = getState().importDashboard.inputs;
...@@ -118,3 +118,123 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i ...@@ -118,3 +118,123 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i
return { name: val.name, value: val.name, meta: val.meta }; 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<any>, 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);
}, []);
}
...@@ -3,9 +3,9 @@ import { css } from 'emotion'; ...@@ -3,9 +3,9 @@ import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui'; import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime'; import { getLocationSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSection, OnDeleteItems } from '../types'; import { DashboardSection, OnDeleteItems } from '../types';
import { getCheckedUids } from '../utils'; import { getCheckedUids } from '../utils';
import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions';
interface Props { interface Props {
onDeleteItems: OnDeleteItems; onDeleteItems: OnDeleteItems;
...@@ -38,7 +38,7 @@ export const ConfirmDeleteModal: FC<Props> = ({ results, onDeleteItems, isOpen, ...@@ -38,7 +38,7 @@ export const ConfirmDeleteModal: FC<Props> = ({ results, onDeleteItems, isOpen,
} }
const deleteItems = () => { const deleteItems = () => {
backendSrv.deleteFoldersAndDashboards(folders, dashboards).then(() => { deleteFoldersAndDashboards(folders, dashboards).then(() => {
onDismiss(); onDismiss();
// Redirect to /dashboard in case folder was deleted from f/:folder.uid // Redirect to /dashboard in case folder was deleted from f/:folder.uid
getLocationSrv().update({ path: '/dashboards' }); getLocationSrv().update({ path: '/dashboards' });
......
...@@ -5,9 +5,9 @@ import { AppEvents, GrafanaTheme } from '@grafana/data'; ...@@ -5,9 +5,9 @@ import { AppEvents, GrafanaTheme } from '@grafana/data';
import { FolderInfo } from 'app/types'; import { FolderInfo } from 'app/types';
import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSection, OnMoveItems } from '../types'; import { DashboardSection, OnMoveItems } from '../types';
import { getCheckedDashboards } from '../utils'; import { getCheckedDashboards } from '../utils';
import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
interface Props { interface Props {
onMoveItems: OnMoveItems; onMoveItems: OnMoveItems;
...@@ -26,7 +26,7 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD ...@@ -26,7 +26,7 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD
if (folder && selectedDashboards.length) { if (folder && selectedDashboards.length) {
const folderTitle = folder.title ?? 'General'; 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) { if (result.successCount > 0) {
const ending = result.successCount === 1 ? '' : 's'; const ending = result.successCount === 1 ? '' : 's';
const header = `Dashboard${ending} Moved`; const header = `Dashboard${ending} Moved`;
......
...@@ -9,12 +9,11 @@ import { ...@@ -9,12 +9,11 @@ import {
DataQuery, DataQuery,
FieldType, FieldType,
} from '@grafana/data'; } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime';
import { Observable, from, of } from 'rxjs'; import { Observable, from, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DatasourceRequestOptions } from 'app/core/services/backend_srv';
import { serializeParams } from 'app/core/utils/fetch'; import { serializeParams } from 'app/core/utils/fetch';
export type JaegerQuery = { export type JaegerQuery = {
...@@ -87,7 +86,7 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> { ...@@ -87,7 +86,7 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
return query.query; return query.query;
} }
private _request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> { private _request(apiUrl: string, data?: any, options?: Partial<BackendSrvRequest>): Observable<Record<string, any>> {
// Hack for proxying metadata requests // Hack for proxying metadata requests
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`; const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
const params = data ? serializeParams(data) : ''; const params = data ? serializeParams(data) : '';
......
...@@ -5,9 +5,8 @@ import { map, filter, catchError, switchMap } from 'rxjs/operators'; ...@@ -5,9 +5,8 @@ import { map, filter, catchError, switchMap } from 'rxjs/operators';
// Services & Utils // Services & Utils
import { DataFrame, dateMath, FieldCache, QueryResultMeta } from '@grafana/data'; 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 { 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 { TemplateSrv } from 'app/features/templating/template_srv';
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore'; import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
import { lokiResultsToTableModel, processRangeQueryResponse, lokiStreamResultToDataFrame } from './result_transformer'; import { lokiResultsToTableModel, processRangeQueryResponse, lokiStreamResultToDataFrame } from './result_transformer';
...@@ -72,7 +71,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { ...@@ -72,7 +71,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
this.maxLines = parseInt(settingsData.maxLines ?? '0', 10) || DEFAULT_MAX_LINES; this.maxLines = parseInt(settingsData.maxLines ?? '0', 10) || DEFAULT_MAX_LINES;
} }
_request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> { _request(apiUrl: string, data?: any, options?: Partial<BackendSrvRequest>): Observable<Record<string, any>> {
const baseUrl = this.instanceSettings.url; const baseUrl = this.instanceSettings.url;
const params = data ? serializeParams(data) : ''; const params = data ? serializeParams(data) : '';
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`; const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
......
// Libraries // Libraries
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import defaults from 'lodash/defaults'; import defaults from 'lodash/defaults';
import $ from 'jquery';
// Services & Utils // Services & Utils
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { import {
...@@ -130,7 +129,6 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -130,7 +129,6 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
} }
} else { } else {
options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
options.transformRequest = (data: any) => $.param(data);
options.data = data; options.data = data;
} }
......
...@@ -17,6 +17,7 @@ import { Scenario, TestDataQuery } from './types'; ...@@ -17,6 +17,7 @@ import { Scenario, TestDataQuery } from './types';
import { getBackendSrv, toDataQueryError } from '@grafana/runtime'; import { getBackendSrv, toDataQueryError } from '@grafana/runtime';
import { queryMetricTree } from './metricTree'; import { queryMetricTree } from './metricTree';
import { from, merge, Observable, of } from 'rxjs'; import { from, merge, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { runStream } from './runStreams'; import { runStream } from './runStreams';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
import { getSearchFilterScopedVar } from 'app/features/variables/utils'; import { getSearchFilterScopedVar } from 'app/features/variables/utils';
...@@ -55,8 +56,8 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> { ...@@ -55,8 +56,8 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
} }
if (queries.length) { if (queries.length) {
const req: Promise<DataQueryResponse> = getBackendSrv() const stream = getBackendSrv()
.datasourceRequest({ .fetch({
method: 'POST', method: 'POST',
url: '/api/tsdb/query', url: '/api/tsdb/query',
data: { data: {
...@@ -64,12 +65,10 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> { ...@@ -64,12 +65,10 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
to: options.range.to.valueOf().toString(), to: options.range.to.valueOf().toString(),
queries: queries, 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); return merge(...streams);
......
...@@ -8,9 +8,8 @@ import { ...@@ -8,9 +8,8 @@ import {
FieldType, FieldType,
} from '@grafana/data'; } from '@grafana/data';
import { from, Observable, of } from 'rxjs'; import { from, Observable, of } from 'rxjs';
import { DatasourceRequestOptions } from '../../../core/services/backend_srv';
import { serializeParams } from '../../../core/utils/fetch'; import { serializeParams } from '../../../core/utils/fetch';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { apiPrefix } from './constants'; import { apiPrefix } from './constants';
import { ZipkinSpan } from './types'; import { ZipkinSpan } from './types';
...@@ -48,7 +47,7 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> { ...@@ -48,7 +47,7 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
return query.query; return query.query;
} }
private request<T = any>(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<{ data: T }> { private request<T = any>(apiUrl: string, data?: any, options?: Partial<BackendSrvRequest>): Observable<{ data: T }> {
// Hack for proxying metadata requests // Hack for proxying metadata requests
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`; const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
const params = data ? serializeParams(data) : ''; const params = data ? serializeParams(data) : '';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment