mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
Influxdb: Fix issues with request creation and parsing (#21743)
This commit is contained in:
parent
7569a8608a
commit
c3a19c6d98
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -81,6 +81,7 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
||||
this.setState({ measurements });
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : error;
|
||||
console.error(error);
|
||||
this.setState({ error: message });
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +204,9 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
|
||||
metricFindQuery(query: string, options?: any) {
|
||||
const interpolated = this.templateSrv.replace(query, null, 'regex');
|
||||
|
||||
return this._seriesQuery(interpolated, options).then(() => 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<InfluxQuery, InfluxO
|
||||
return result.data;
|
||||
},
|
||||
(err: any) => {
|
||||
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<InfluxQuery, InfluxO
|
||||
config: err.config,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -31,7 +31,7 @@ describe('InfluxDataSource', () => {
|
||||
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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user