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
......@@ -16,8 +16,9 @@ The request function can be used to perform a remote call by specifying a [Backe
<b>Signature</b>
```typescript
export interface BackendSrv
export interface BackendSrv
```
<b>Import</b>
```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
<b>Methods</b>
| 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. |
| [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 <code>option.silent</code> 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<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>BackendSrvRequest</code> | |
| Parameter | Type | Description |
| --------- | ------------------------------ | ----------- |
| options | <code>BackendSrvRequest</code> | |
<b>Returns:</b>
......@@ -66,11 +70,12 @@ datasourceRequest(options: BackendSrvRequest): Promise<any>;
```typescript
delete(url: string): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | |
| Parameter | Type | Description |
| --------- | ------------------- | ----------- |
| url | <code>string</code> | |
<b>Returns:</b>
......@@ -83,13 +88,14 @@ delete(url: string): Promise<any>;
```typescript
get(url: string, params?: any, requestId?: string): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | |
| params | <code>any</code> | |
| requestId | <code>string</code> | |
| Parameter | Type | Description |
| --------- | ------------------- | ----------- |
| url | <code>string</code> | |
| params | <code>any</code> | |
| requestId | <code>string</code> | |
<b>Returns:</b>
......@@ -102,12 +108,13 @@ get(url: string, params?: any, requestId?: string): Promise<any>;
```typescript
patch(url: string, data?: any): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | |
| data | <code>any</code> | |
| Parameter | Type | Description |
| --------- | ------------------- | ----------- |
| url | <code>string</code> | |
| data | <code>any</code> | |
<b>Returns:</b>
......@@ -120,12 +127,13 @@ patch(url: string, data?: any): Promise<any>;
```typescript
post(url: string, data?: any): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | |
| data | <code>any</code> | |
| Parameter | Type | Description |
| --------- | ------------------- | ----------- |
| url | <code>string</code> | |
| data | <code>any</code> | |
<b>Returns:</b>
......@@ -138,12 +146,13 @@ post(url: string, data?: any): Promise<any>;
```typescript
put(url: string, data?: any): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | |
| data | <code>any</code> | |
| Parameter | Type | Description |
| --------- | ------------------- | ----------- |
| url | <code>string</code> | |
| data | <code>any</code> | |
<b>Returns:</b>
......@@ -156,13 +165,13 @@ put(url: string, data?: any): Promise<any>;
```typescript
request(options: BackendSrvRequest): Promise<any>;
```
<b>Parameters</b>
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>BackendSrvRequest</code> | |
| Parameter | Type | Description |
| --------- | ------------------------------ | ----------- |
| options | <code>BackendSrvRequest</code> | |
<b>Returns:</b>
`Promise<any>`
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<string, any>;
/**
* HTTP verb to perform in the remote call GET, POST, PUT etc.
......@@ -23,22 +29,88 @@ 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
* exist the backendSrv will try to cancel and replace the previous call with the
* 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<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,
* 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.
......@@ -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<any>;
patch(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>;
/**
* @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<any>;
/**
* Observable http request interface
*/
fetch<T>(options: BackendSrvRequest): Observable<FetchResponse<T>>;
}
let singletonInstance: BackendSrv;
......
......@@ -171,12 +171,11 @@ export class DataSourceWithBackend<
*/
async callHealthCheck(): Promise<HealthCheckResult> {
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;
});
}
......
......@@ -38,11 +38,12 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> {
dispatch(userAdminPageLoadedAction(true));
} catch (error) {
console.log(error);
error.isHandled = true;
const userError = {
title: error.data.message,
body: error.data.error,
};
dispatch(userAdminPageFailedAction(userError));
}
};
......
......@@ -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('/');
})
......
......@@ -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();
......
......@@ -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<Props, State> {
formattedJson: any;
clipboard: any;
subscription?: Unsubscribable;
constructor(props: Props) {
super(props);
......@@ -56,8 +58,10 @@ export class QueryInspector extends PureComponent<Props, State> {
}
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<Props, State> {
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<Props, State> {
}
});
}
this.setState({ executedQueries });
}
......@@ -105,23 +114,11 @@ export class QueryInspector extends PureComponent<Props, State> {
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<Props, State> {
}));
};
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<Props, State> {
response: response,
},
}));
};
}
setFormattedJson = (formattedJson: any) => {
this.formattedJson = formattedJson;
......
......@@ -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) => {
......
......@@ -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,
});
}
......
......@@ -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
......
......@@ -33,7 +33,7 @@ export function saveFolder(folder: FolderState): ThunkResult<void> {
export function deleteFolder(uid: string): ThunkResult<void> {
return async dispatch => {
await backendSrv.deleteFolder(uid, true);
await backendSrv.delete(`/api/folders/${uid}`);
dispatch(updateLocation({ path: `dashboards` }));
};
}
......
......@@ -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<Props, State> {
};
onSubmit = (form: ImportDashboardDTO) => {
this.props.saveDashboard(form);
this.props.importDashboard(form);
};
onCancel = () => {
......@@ -116,7 +116,7 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
clearLoadedDashboard,
saveDashboard,
importDashboard,
};
export const ImportDashboardOverview = connect(mapStateToProps, mapDispatchToProps)(ImportDashboardOverviewUnConnected);
......
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<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) => {
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<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';
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<Props> = ({ 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' });
......
......@@ -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<Props> = ({ 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`;
......
......@@ -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<JaegerQuery> {
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
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
const params = data ? serializeParams(data) : '';
......
......@@ -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<LokiQuery, LokiOptions> {
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 params = data ? serializeParams(data) : '';
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
......
// 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<PromQuery, PromOptions>
}
} else {
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
options.transformRequest = (data: any) => $.param(data);
options.data = data;
}
......
......@@ -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<TestDataQuery> {
}
if (queries.length) {
const req: Promise<DataQueryResponse> = getBackendSrv()
.datasourceRequest({
const stream = getBackendSrv()
.fetch({
method: 'POST',
url: '/api/tsdb/query',
data: {
......@@ -64,12 +65,10 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
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);
......
......@@ -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<ZipkinQuery> {
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
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
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