mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BackendSrv: Adds missing props back to response object in datasourceRequest (#21727)
* BackendSrv: Adds status, headers, statusText, redirect, type and url back to response Fixes #21662 * BackendSrv: Adds request object back to datasourceRequest response Fixes #21662
This commit is contained in:
@@ -13,6 +13,7 @@ import { CoreEvents, DashboardDTO, FolderInfo } from 'app/types';
|
||||
import { ContextSrv, contextSrv } from './context_srv';
|
||||
import { coreModule } from 'app/core/core_module';
|
||||
import { Emitter } from '../utils/emitter';
|
||||
import { DataSourceResponse } from '../../types/events';
|
||||
|
||||
export interface DatasourceRequestOptions {
|
||||
retry?: number;
|
||||
@@ -34,18 +35,11 @@ interface ErrorResponseProps extends FetchResponseProps {
|
||||
error?: string | any;
|
||||
}
|
||||
|
||||
export interface FetchResponse<T extends FetchResponseProps = any> {
|
||||
status: number;
|
||||
statusText: string;
|
||||
ok: boolean;
|
||||
data: T;
|
||||
}
|
||||
export interface FetchResponse<T extends FetchResponseProps = any> extends DataSourceResponse<T> {}
|
||||
|
||||
interface SuccessResponse extends FetchResponseProps, Record<any, any> {}
|
||||
|
||||
interface DataSourceSuccessResponse<T extends {} = any> {
|
||||
data: T;
|
||||
}
|
||||
interface DataSourceSuccessResponse<T extends {} = any> extends FetchResponse<T> {}
|
||||
|
||||
interface ErrorResponse<T extends ErrorResponseProps = any> {
|
||||
status: number;
|
||||
@@ -212,8 +206,7 @@ export class BackendSrv implements BackendService {
|
||||
const successStream = fromFetchStream.pipe(
|
||||
filter(response => response.ok === true),
|
||||
map(response => {
|
||||
const { data } = response;
|
||||
const fetchSuccessResponse: DataSourceSuccessResponse = { data };
|
||||
const fetchSuccessResponse: DataSourceSuccessResponse = { ...response };
|
||||
return fetchSuccessResponse;
|
||||
}),
|
||||
tap(res => {
|
||||
@@ -482,7 +475,7 @@ export class BackendSrv implements BackendService {
|
||||
const init = this.parseInitFromOptions(options);
|
||||
return this.dependencies.fromFetch(url, init).pipe(
|
||||
mergeMap(async response => {
|
||||
const { status, statusText, ok } = 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 {
|
||||
@@ -490,7 +483,17 @@ export class BackendSrv implements BackendService {
|
||||
} catch {
|
||||
data = textData;
|
||||
}
|
||||
const fetchResponse: FetchResponse = { status, statusText, ok, data };
|
||||
const fetchResponse: FetchResponse = {
|
||||
status,
|
||||
statusText,
|
||||
ok,
|
||||
data,
|
||||
headers,
|
||||
url,
|
||||
type,
|
||||
redirected,
|
||||
request: { url, ...init },
|
||||
};
|
||||
return fetchResponse;
|
||||
}),
|
||||
share() // sharing this so we can split into success and failure and then merge back
|
||||
|
||||
@@ -14,16 +14,25 @@ const getTestContext = (overides?: object) => {
|
||||
statusText: 'Ok',
|
||||
isSignedIn: true,
|
||||
orgId: 1337,
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: 'http://localhost:3000/api/some-mock',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
const props = { ...defaults, ...overides };
|
||||
const textMock = jest.fn().mockResolvedValue(JSON.stringify(props.data));
|
||||
const fromFetchMock = jest.fn().mockImplementation(() => {
|
||||
return of({
|
||||
const mockedResponse = {
|
||||
ok: props.ok,
|
||||
status: props.status,
|
||||
statusText: props.statusText,
|
||||
text: textMock,
|
||||
});
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: 'http://localhost:3000/api/some-mock',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return of(mockedResponse);
|
||||
});
|
||||
const appEventsMock: Emitter = ({
|
||||
emit: jest.fn(),
|
||||
@@ -38,8 +47,11 @@ const getTestContext = (overides?: object) => {
|
||||
const logoutMock = jest.fn();
|
||||
const parseRequestOptionsMock = jest.fn().mockImplementation(options => options);
|
||||
const parseDataSourceRequestOptionsMock = jest.fn().mockImplementation(options => options);
|
||||
const parseUrlFromOptionsMock = jest.fn().mockImplementation(() => 'parseUrlFromOptionsMock');
|
||||
const parseInitFromOptionsMock = jest.fn().mockImplementation(() => 'parseInitFromOptionsMock');
|
||||
const parseUrlFromOptionsMock = jest.fn().mockImplementation(options => options.url);
|
||||
const parseInitFromOptionsMock = jest.fn().mockImplementation(options => ({
|
||||
method: options.method,
|
||||
url: options.url,
|
||||
}));
|
||||
|
||||
const backendSrv = new BackendSrv({
|
||||
fromFetch: fromFetchMock,
|
||||
@@ -59,7 +71,10 @@ const getTestContext = (overides?: object) => {
|
||||
expect(parseInitFromOptionsMock).toHaveBeenCalledTimes(1);
|
||||
expect(parseInitFromOptionsMock).toHaveBeenCalledWith(options);
|
||||
expect(fromFetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fromFetchMock).toHaveBeenCalledWith('parseUrlFromOptionsMock', 'parseInitFromOptionsMock');
|
||||
expect(fromFetchMock).toHaveBeenCalledWith(options.url, {
|
||||
method: options.method,
|
||||
url: options.url,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRequestCallChain = (options: any) => {
|
||||
@@ -321,32 +336,67 @@ describe('backendSrv', () => {
|
||||
describe('datasourceRequest', () => {
|
||||
describe('when making a successful call and silent is true', () => {
|
||||
it('then it should not emit message', async () => {
|
||||
const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext();
|
||||
const url = 'http://www.some.url.com/';
|
||||
const result = await backendSrv.datasourceRequest({ url, silent: true });
|
||||
expect(result).toEqual({ data: { test: 'hello world' } });
|
||||
const url = 'http://localhost:3000/api/some-mock';
|
||||
const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext({ url });
|
||||
const result = await backendSrv.datasourceRequest({ url, method: 'GET', silent: true });
|
||||
expect(result).toEqual({
|
||||
data: { test: 'hello world' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
type: 'basic',
|
||||
url,
|
||||
request: { url, method: 'GET' },
|
||||
});
|
||||
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
||||
expectDataSourceRequestCallChain({ url, silent: true });
|
||||
expectDataSourceRequestCallChain({ 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 { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext();
|
||||
const url = 'http://www.some.url.com/';
|
||||
const result = await backendSrv.datasourceRequest({ url });
|
||||
expect(result).toEqual({ data: { test: 'hello world' } });
|
||||
const url = 'http://localhost:3000/api/some-mock';
|
||||
const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext({ url });
|
||||
const result = await backendSrv.datasourceRequest({ url, method: 'GET' });
|
||||
expect(result).toEqual({
|
||||
data: { test: 'hello world' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
type: 'basic',
|
||||
url,
|
||||
request: { url, method: 'GET' },
|
||||
});
|
||||
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
|
||||
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestResponse, {
|
||||
data: { test: 'hello world' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
type: 'basic',
|
||||
url,
|
||||
request: { url, method: 'GET' },
|
||||
});
|
||||
expectDataSourceRequestCallChain({ url });
|
||||
expectDataSourceRequestCallChain({ url, method: 'GET' });
|
||||
});
|
||||
});
|
||||
|
||||
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 { backendSrv, fromFetchMock } = getTestContext();
|
||||
const url = '/api/dashboard/';
|
||||
const { backendSrv, fromFetchMock } = getTestContext({ url });
|
||||
const unsubscribe = jest.fn();
|
||||
const slowData = { message: 'Slow Request' };
|
||||
const slowFetch = new Observable(subscriber => {
|
||||
@@ -355,6 +405,12 @@ describe('backendSrv', () => {
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
text: () => Promise.resolve(JSON.stringify(slowData)),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url,
|
||||
});
|
||||
return unsubscribe;
|
||||
}).pipe(delay(10000));
|
||||
@@ -364,16 +420,35 @@ describe('backendSrv', () => {
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
text: () => Promise.resolve(JSON.stringify(fastData)),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url,
|
||||
});
|
||||
fromFetchMock.mockImplementationOnce(() => slowFetch);
|
||||
fromFetchMock.mockImplementation(() => fastFetch);
|
||||
const options = {
|
||||
url: '/api/dashboard/',
|
||||
url,
|
||||
method: 'GET',
|
||||
requestId: 'A',
|
||||
};
|
||||
const slowRequest = backendSrv.datasourceRequest(options);
|
||||
const fastResponse = await backendSrv.datasourceRequest(options);
|
||||
expect(fastResponse).toEqual({ data: { message: 'Fast Request' } });
|
||||
expect(fastResponse).toEqual({
|
||||
data: { message: 'Fast Request' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
type: 'basic',
|
||||
url,
|
||||
request: { url, method: 'GET' },
|
||||
});
|
||||
|
||||
const slowResponse = await slowRequest;
|
||||
expect(slowResponse).toEqual(undefined);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||
import { LoadingPlaceholder, JSONFormatter } from '@grafana/ui';
|
||||
import { JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { AppEvents, PanelEvents } from '@grafana/data';
|
||||
|
||||
@@ -96,9 +96,7 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
delete response.headers;
|
||||
}
|
||||
|
||||
if (response.config) {
|
||||
response.request = response.config;
|
||||
delete response.config;
|
||||
if (response.request) {
|
||||
delete response.request.transformRequest;
|
||||
delete response.request.transformResponse;
|
||||
delete response.request.paramSerializer;
|
||||
@@ -116,6 +114,10 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
delete response.data;
|
||||
delete response.status;
|
||||
delete response.statusText;
|
||||
delete response.ok;
|
||||
delete response.url;
|
||||
delete response.redirected;
|
||||
delete response.type;
|
||||
delete response.$$config;
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DataFrame, eventFactory, TimeRange } from '@grafana/data';
|
||||
import { IHttpResponse } from 'angular';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,19 @@ export interface ShowConfirmModalPayload {
|
||||
onAltAction?: () => void;
|
||||
}
|
||||
|
||||
type DataSourceResponsePayload = IHttpResponse<any>;
|
||||
export interface DataSourceResponse<T> {
|
||||
data: T;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly ok: boolean;
|
||||
readonly headers: Headers;
|
||||
readonly redirected: boolean;
|
||||
readonly type: ResponseType;
|
||||
readonly url: string;
|
||||
readonly request: any;
|
||||
}
|
||||
|
||||
type DataSourceResponsePayload = DataSourceResponse<any>;
|
||||
|
||||
export interface SaveDashboardPayload {
|
||||
overwrite?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user