diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 9c92a46a44d..796f123513d 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -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 { - status: number; - statusText: string; - ok: boolean; - data: T; -} +export interface FetchResponse extends DataSourceResponse {} interface SuccessResponse extends FetchResponseProps, Record {} -interface DataSourceSuccessResponse { - data: T; -} +interface DataSourceSuccessResponse extends FetchResponse {} interface ErrorResponse { 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 diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index 7776b81ad53..e6e9f008d19 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -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); diff --git a/public/app/features/dashboard/panel_editor/QueryInspector.tsx b/public/app/features/dashboard/panel_editor/QueryInspector.tsx index a4ea5af09ae..5152aa8046f 100644 --- a/public/app/features/dashboard/panel_editor/QueryInspector.tsx +++ b/public/app/features/dashboard/panel_editor/QueryInspector.tsx @@ -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 { 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 { 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 => ({ diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 229f2f9cf93..db9f0580da8 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -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; +export interface DataSourceResponse { + 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; export interface SaveDashboardPayload { overwrite?: boolean;