Commit 18788727 by Hugo Häggmark Committed by GitHub

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
parent 0fda3c4f
......@@ -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);
......
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 => ({
......
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;
......
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