diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 698a6b2dd1b..eac77a65cc9 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -454,42 +454,9 @@ export class BackendSrv implements BackendService { return options; }; - private parseUrlFromOptions = (options: BackendSrvRequest): string => { - const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0)); - const serializedParams = serializeParams(cleanParams); - return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url; - }; - - private parseInitFromOptions = (options: BackendSrvRequest): RequestInit => { - const method = options.method; - const headers = { - 'Content-Type': 'application/json', - Accept: 'application/json, text/plain, */*', - ...options.headers, - }; - const body = this.parseBody({ ...options, headers }); - return { - method, - headers, - body, - }; - }; - - private parseBody = (options: BackendSrvRequest) => { - if (!options.data || typeof options.data === 'string') { - return options.data; - } - - if (options.headers['Content-Type'] === 'application/json') { - return JSON.stringify(options.data); - } - - return new URLSearchParams(options.data); - }; - private getFromFetchStream = (options: BackendSrvRequest) => { - const url = this.parseUrlFromOptions(options); - const init = this.parseInitFromOptions(options); + const url = parseUrlFromOptions(options); + const init = parseInitFromOptions(options); return this.dependencies.fromFetch(url, init).pipe( mergeMap(async response => { const { status, statusText, ok, headers, url, type, redirected } = response; @@ -545,3 +512,36 @@ coreModule.factory('backendSrv', () => backendSrv); // Used for testing and things that really need BackendSrv export const backendSrv = new BackendSrv(); export const getBackendSrv = (): BackendSrv => backendSrv; + +export const parseUrlFromOptions = (options: BackendSrvRequest): string => { + const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0)); + const serializedParams = serializeParams(cleanParams); + return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url; +}; + +export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => { + const method = options.method; + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json, text/plain, */*', + ...options.headers, + }; + const body = parseBody({ ...options, headers }); + return { + method, + headers, + body, + }; +}; + +const parseBody = (options: BackendSrvRequest) => { + if (!options.data || typeof options.data === 'string') { + return options.data; + } + + if (options.headers['Content-Type'] === 'application/json') { + return JSON.stringify(options.data); + } + + return new URLSearchParams(options.data); +}; diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index 2f580f5f377..2157daef63f 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -1,4 +1,4 @@ -import { BackendSrv, getBackendSrv } from '../services/backend_srv'; +import { BackendSrv, getBackendSrv, parseInitFromOptions, parseUrlFromOptions } from '../services/backend_srv'; import { Emitter } from '../utils/emitter'; import { ContextSrv, User } from '../services/context_srv'; import { Observable, of } from 'rxjs'; @@ -47,11 +47,6 @@ 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(options => options.url); - const parseInitFromOptionsMock = jest.fn().mockImplementation(options => ({ - method: options.method, - url: options.url, - })); const backendSrv = new BackendSrv({ fromFetch: fromFetchMock, @@ -62,19 +57,9 @@ const getTestContext = (overides?: object) => { backendSrv['parseRequestOptions'] = parseRequestOptionsMock; backendSrv['parseDataSourceRequestOptions'] = parseDataSourceRequestOptionsMock; - backendSrv['parseUrlFromOptions'] = parseUrlFromOptionsMock; - backendSrv['parseInitFromOptions'] = parseInitFromOptionsMock; const expectCallChain = (options: any) => { - expect(parseUrlFromOptionsMock).toHaveBeenCalledTimes(1); - expect(parseUrlFromOptionsMock).toHaveBeenCalledWith(options); - expect(parseInitFromOptionsMock).toHaveBeenCalledTimes(1); - expect(parseInitFromOptionsMock).toHaveBeenCalledWith(options); expect(fromFetchMock).toHaveBeenCalledTimes(1); - expect(fromFetchMock).toHaveBeenCalledWith(options.url, { - method: options.method, - url: options.url, - }); }; const expectRequestCallChain = (options: any) => { @@ -98,8 +83,6 @@ const getTestContext = (overides?: object) => { logoutMock, parseRequestOptionsMock, parseDataSourceRequestOptionsMock, - parseUrlFromOptionsMock, - parseInitFromOptionsMock, expectRequestCallChain, expectDataSourceRequestCallChain, }; @@ -150,45 +133,6 @@ describe('backendSrv', () => { ); }); - describe('parseUrlFromOptions', () => { - it.each` - params | url | expected - ${undefined} | ${'api/dashboard'} | ${'api/dashboard'} - ${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'} - ${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'} - ${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'} - ${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'} - ${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'} - ${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'} - `( - "when called with params: '$params' and url: '$url' then result should be '$expected'", - ({ params, url, expected }) => { - expect(getBackendSrv()['parseUrlFromOptions']({ params, url })).toEqual(expected); - } - ); - }); - - describe('parseInitFromOptions', () => { - it.each` - method | headers | data | expected - ${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} - ${'GET'} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} - ${'GET'} | ${{ Auth: 'Some Auth' }} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }} - ${'GET'} | ${{ Auth: 'Some Auth' }} | ${{ data: { test: 'Some data' } }} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} - ${'GET'} | ${{ Auth: 'Some Auth' }} | ${'some data'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: 'some data' }} - ${'GET'} | ${{ Auth: 'Some Auth' }} | ${'{"data":{"test":"Some data"}}'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} - ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${undefined} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }} - ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${{ data: 'Some data' }} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: new URLSearchParams({ data: 'Some data' }) }} - ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${'some data'} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: 'some data' }} - ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${'{"data":{"test":"Some data"}}'} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} - `( - "when called with method: '$method', headers: '$headers' and data: '$data' then result should be '$expected'", - ({ method, headers, data, expected }) => { - expect(getBackendSrv()['parseInitFromOptions']({ method, headers, data, url: '' })).toEqual(expected); - } - ); - }); - describe('request', () => { describe('when making a successful call and conditions for showSuccessAlert are not favorable', () => { it('then it should return correct result and not emit anything', async () => { @@ -354,7 +298,15 @@ describe('backendSrv', () => { statusText: 'Ok', type: 'basic', url, - request: { url, method: 'GET' }, + request: { + url, + method: 'GET', + body: undefined, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/plain, */*', + }, + }, }); expect(appEventsMock.emit).not.toHaveBeenCalled(); expectDataSourceRequestCallChain({ url, method: 'GET', silent: true }); @@ -366,7 +318,7 @@ describe('backendSrv', () => { 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({ + const expectedResult = { data: { test: 'hello world' }, headers: { 'Content-Type': 'application/json', @@ -377,22 +329,20 @@ describe('backendSrv', () => { statusText: 'Ok', type: 'basic', url, - request: { url, method: 'GET' }, - }); + request: { + url, + method: 'GET', + body: undefined as any, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/plain, */*', + }, + }, + }; + + expect(result).toEqual(expectedResult); 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' }, - }); + expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestResponse, expectedResult); expectDataSourceRequestCallChain({ url, method: 'GET' }); }); }); @@ -451,7 +401,15 @@ describe('backendSrv', () => { statusText: 'Ok', type: 'basic', url, - request: { url, method: 'GET' }, + request: { + url, + method: 'GET', + body: undefined, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/plain, */*', + }, + }, }); const slowResponse = await slowRequest; @@ -610,3 +568,43 @@ describe('backendSrv', () => { }); }); }); + +describe('parseUrlFromOptions', () => { + it.each` + params | url | expected + ${undefined} | ${'api/dashboard'} | ${'api/dashboard'} + ${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'} + ${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'} + ${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'} + ${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'} + ${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'} + ${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'} + `( + "when called with params: '$params' and url: '$url' then result should be '$expected'", + ({ params, url, expected }) => { + expect(parseUrlFromOptions({ params, url })).toEqual(expected); + } + ); +}); + +describe('parseInitFromOptions', () => { + it.each` + method | headers | data | expected + ${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} + ${'GET'} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} + ${'GET'} | ${undefined} | ${null} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: null }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${{ data: { test: 'Some data' } }} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${'some data'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: 'some data' }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${'{"data":{"test":"Some data"}}'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} + ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${undefined} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }} + ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${{ data: 'Some data' }} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: new URLSearchParams({ data: 'Some data' }) }} + ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${'some data'} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: 'some data' }} + ${'POST'} | ${{ Auth: 'Some Auth', 'Content-Type': 'application/x-www-form-urlencoded' }} | ${'{"data":{"test":"Some data"}}'} | ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} + `( + "when called with method: '$method', headers: '$headers' and data: '$data' then result should be '$expected'", + ({ method, headers, data, expected }) => { + expect(parseInitFromOptions({ method, headers, data, url: '' })).toEqual(expected); + } + ); +}); diff --git a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx index df2fae943ad..c3e4b579492 100644 --- a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx +++ b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx @@ -81,6 +81,7 @@ export class InfluxLogsQueryField extends React.PureComponent { this.setState({ measurements }); } catch (error) { const message = error && error.message ? error.message : error; + console.error(error); this.setState({ error: message }); } } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 116931e478d..06cd07885ae 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -204,7 +204,9 @@ export default class InfluxDatasource extends DataSourceApi this.responseParser.parse(query)); + return this._seriesQuery(interpolated, options).then(resp => { + return this.responseParser.parse(query, resp); + }); } getTagKeys(options: any = {}) { @@ -323,7 +325,7 @@ export default class InfluxDatasource extends DataSourceApi { - if (err.status !== 0 || err.status >= 300) { + if ((Number.isInteger(err.status) && err.status !== 0) || err.status >= 300) { if (err.data && err.data.error) { throw { message: 'InfluxDB Error: ' + err.data.error, @@ -337,6 +339,8 @@ export default class InfluxDatasource extends DataSourceApi { to: '2018-01-02T00:00:00Z', }, }; - let requestQuery: any, requestMethod: any, requestData: any; + let requestQuery: any, requestMethod: any, requestData: any, response: any; beforeEach(async () => { datasourceRequestMock.mockImplementation((req: any) => { @@ -39,21 +39,23 @@ describe('InfluxDataSource', () => { requestQuery = req.params.q; requestData = req.data; return Promise.resolve({ - results: [ - { - series: [ - { - name: 'measurement', - columns: ['max'], - values: [[1]], - }, - ], - }, - ], + data: { + results: [ + { + series: [ + { + name: 'measurement', + columns: ['name'], + values: [['cpu']], + }, + ], + }, + ], + }, }); }); - await ctx.ds.metricFindQuery(query, queryOptions).then(() => {}); + response = await ctx.ds.metricFindQuery(query, queryOptions); }); it('should replace $timefilter', () => { @@ -67,6 +69,10 @@ describe('InfluxDataSource', () => { it('should not have any data in request body', () => { expect(requestData).toBeNull(); }); + + it('parse response correctly', () => { + expect(response).toEqual([{ text: 'cpu' }]); + }); }); describe('InfluxDataSource in POST query mode', () => {