mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Mysql: Support request cancellation properly (Uses new backendSrv.fetch Observable request API) (#27649)
This commit is contained in:
parent
3ae6ba034d
commit
0bb8b32821
@ -1,12 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import ResponseParser from './response_parser';
|
||||
import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map, mapTo } from 'rxjs/operators';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
//Types
|
||||
import { MysqlQueryForInterpolation } from './types';
|
||||
import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query';
|
||||
import ResponseParser, { MysqlResponse } from './response_parser';
|
||||
import { MysqlMetricFindValue, MysqlQueryForInterpolation } from './types';
|
||||
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
|
||||
|
||||
export class MysqlDatasource {
|
||||
@ -25,7 +27,7 @@ export class MysqlDatasource {
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval || '1m';
|
||||
}
|
||||
|
||||
interpolateVariable = (value: string, variable: any) => {
|
||||
interpolateVariable = (value: string | string[] | number, variable: any) => {
|
||||
if (typeof value === 'string') {
|
||||
if (variable.multi || variable.includeAll) {
|
||||
const result = this.queryModel.quoteLiteral(value);
|
||||
@ -64,7 +66,7 @@ export class MysqlDatasource {
|
||||
return expandedQueries;
|
||||
}
|
||||
|
||||
query(options: any) {
|
||||
query(options: any): Observable<MysqlResponse> {
|
||||
const queries = _.filter(options.targets, target => {
|
||||
return target.hide !== true;
|
||||
}).map(target => {
|
||||
@ -81,11 +83,11 @@ export class MysqlDatasource {
|
||||
});
|
||||
|
||||
if (queries.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
return of({ data: [] });
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.datasourceRequest({
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
@ -94,7 +96,7 @@ export class MysqlDatasource {
|
||||
queries: queries,
|
||||
},
|
||||
})
|
||||
.then(this.responseParser.processQueryResult);
|
||||
.pipe(map(this.responseParser.processQueryResult));
|
||||
}
|
||||
|
||||
annotationQuery(options: any) {
|
||||
@ -112,7 +114,7 @@ export class MysqlDatasource {
|
||||
};
|
||||
|
||||
return getBackendSrv()
|
||||
.datasourceRequest({
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
@ -121,10 +123,11 @@ export class MysqlDatasource {
|
||||
queries: [query],
|
||||
},
|
||||
})
|
||||
.then((data: any) => this.responseParser.transformAnnotationResponse(options, data));
|
||||
.pipe(map((data: any) => this.responseParser.transformAnnotationResponse(options, data)))
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, optionalOptions: any) {
|
||||
metricFindQuery(query: string, optionalOptions: any): Promise<MysqlMetricFindValue[]> {
|
||||
let refId = 'tempvar';
|
||||
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
|
||||
refId = optionalOptions.variable.name;
|
||||
@ -158,17 +161,18 @@ export class MysqlDatasource {
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.datasourceRequest({
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
})
|
||||
.then((data: any) => this.responseParser.parseMetricFindQueryResult(refId, data));
|
||||
.pipe(map((data: any) => this.responseParser.parseMetricFindQueryResult(refId, data)))
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
return getBackendSrv()
|
||||
.datasourceRequest({
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
@ -186,17 +190,18 @@ export class MysqlDatasource {
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((res: any) => {
|
||||
return { status: 'success', message: 'Database Connection OK' };
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error(err);
|
||||
if (err.data && err.data.message) {
|
||||
return { status: 'error', message: err.data.message };
|
||||
} else {
|
||||
return { status: 'error', message: err.status };
|
||||
}
|
||||
});
|
||||
.pipe(
|
||||
mapTo({ status: 'success', message: 'Database Connection OK' }),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
if (err.data && err.data.message) {
|
||||
return of({ status: 'error', message: err.data.message });
|
||||
} else {
|
||||
return of({ status: 'error', message: err.status });
|
||||
}
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
targetContainsTemplate(target: any) {
|
||||
|
@ -1,7 +1,25 @@
|
||||
import _ from 'lodash';
|
||||
import { MysqlMetricFindValue } from './types';
|
||||
|
||||
interface TableResponse extends Record<string, any> {
|
||||
type: string;
|
||||
refId: string;
|
||||
meta: any;
|
||||
}
|
||||
|
||||
interface SeriesResponse extends Record<string, any> {
|
||||
target: string;
|
||||
refId: string;
|
||||
meta: any;
|
||||
datapoints: [any[]];
|
||||
}
|
||||
|
||||
export interface MysqlResponse {
|
||||
data: Array<TableResponse | SeriesResponse>;
|
||||
}
|
||||
|
||||
export default class ResponseParser {
|
||||
processQueryResult(res: any) {
|
||||
processQueryResult(res: any): MysqlResponse {
|
||||
const data: any[] = [];
|
||||
|
||||
if (!res.data.results) {
|
||||
@ -35,7 +53,7 @@ export default class ResponseParser {
|
||||
return { data: data };
|
||||
}
|
||||
|
||||
parseMetricFindQueryResult(refId: string, results: any) {
|
||||
parseMetricFindQueryResult(refId: string, results: any): MysqlMetricFindValue[] {
|
||||
if (!results || results.data.length === 0 || results.data.results[refId].meta.rowCount === 0) {
|
||||
return [];
|
||||
}
|
||||
@ -117,9 +135,9 @@ export default class ResponseParser {
|
||||
} else if (table.columns[i].text === 'timeend') {
|
||||
timeEndColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'title') {
|
||||
return Promise.reject({
|
||||
throw {
|
||||
message: 'The title column for annotations is deprecated, now only a column named text is returned',
|
||||
});
|
||||
};
|
||||
} else if (table.columns[i].text === 'text') {
|
||||
textColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'tags') {
|
||||
@ -128,9 +146,9 @@ export default class ResponseParser {
|
||||
}
|
||||
|
||||
if (timeColumnIndex === -1) {
|
||||
return Promise.reject({
|
||||
throw {
|
||||
message: 'Missing mandatory time column (with time_sec column alias) in annotation query.',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const list = [];
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { MysqlDatasource } from '../datasource';
|
||||
import { of } from 'rxjs';
|
||||
import { dateTime, toUtc } from '@grafana/data';
|
||||
|
||||
import { MysqlDatasource } from '../datasource';
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer';
|
||||
import { FetchResponse } from '@grafana/runtime';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
@ -10,35 +13,32 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}));
|
||||
|
||||
describe('MySQLDatasource', () => {
|
||||
const instanceSettings = { name: 'mysql' };
|
||||
const templateSrv: TemplateSrv = new TemplateSrv();
|
||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const raw = {
|
||||
from: toUtc('2018-04-25 10:00'),
|
||||
to: toUtc('2018-04-25 11:00'),
|
||||
};
|
||||
const ctx = {
|
||||
timeSrvMock: {
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
const setupTextContext = (response: any) => {
|
||||
const instanceSettings = { name: 'mysql' };
|
||||
const templateSrv: TemplateSrv = new TemplateSrv();
|
||||
const raw = {
|
||||
from: toUtc('2018-04-25 10:00'),
|
||||
to: toUtc('2018-04-25 11:00'),
|
||||
};
|
||||
const timeSrvMock: any = {
|
||||
timeRange: () => ({
|
||||
from: raw.from,
|
||||
to: raw.to,
|
||||
raw: raw,
|
||||
}),
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
const variable = { ...initialCustomVariableModelState };
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.ds = new MysqlDatasource(instanceSettings, templateSrv, ctx.timeSrvMock);
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
fetchMock.mockImplementation(options => of(createFetchResponse(response)));
|
||||
|
||||
const ds = new MysqlDatasource(instanceSettings, templateSrv, timeSrvMock);
|
||||
|
||||
return { ds, variable, templateSrv };
|
||||
};
|
||||
|
||||
describe('When performing annotationQuery', () => {
|
||||
let results: any;
|
||||
|
||||
const annotationName = 'MyAnno';
|
||||
|
||||
const options = {
|
||||
@ -70,15 +70,10 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 }));
|
||||
it('should return annotation list', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.annotationQuery(options);
|
||||
|
||||
ctx.ds.annotationQuery(options).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
|
||||
it('should return annotation list', () => {
|
||||
expect(results.length).toBe(3);
|
||||
|
||||
expect(results[0].text).toBe('some text');
|
||||
@ -93,7 +88,6 @@ describe('MySQLDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery', () => {
|
||||
let results: any;
|
||||
const query = 'select * from atable';
|
||||
const response = {
|
||||
results: {
|
||||
@ -116,15 +110,10 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 }));
|
||||
it('should return list of all column values', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, {});
|
||||
|
||||
ctx.ds.metricFindQuery(query).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
|
||||
it('should return list of all column values', () => {
|
||||
expect(results.length).toBe(6);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[5].text).toBe('some text3');
|
||||
@ -132,8 +121,6 @@ describe('MySQLDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery with $__searchFilter and a searchFilter is given', () => {
|
||||
let results: any;
|
||||
let calledWith: any = {};
|
||||
const query = "select title from atable where title LIKE '$__searchFilter'";
|
||||
const response = {
|
||||
results: {
|
||||
@ -156,26 +143,19 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(options => {
|
||||
calledWith = options;
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query, { searchFilter: 'aTit' }).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
it('should return list of all column values', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, { searchFilter: 'aTit' });
|
||||
|
||||
it('should return list of all column values', () => {
|
||||
expect(datasourceRequestMock).toBeCalledTimes(1);
|
||||
expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE 'aTit%'");
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe(
|
||||
"select title from atable where title LIKE 'aTit%'"
|
||||
);
|
||||
expect(results.length).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery with $__searchFilter but no searchFilter is given', () => {
|
||||
let results: any;
|
||||
let calledWith: any = {};
|
||||
const query = "select title from atable where title LIKE '$__searchFilter'";
|
||||
const response = {
|
||||
results: {
|
||||
@ -198,25 +178,17 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(options => {
|
||||
calledWith = options;
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
ctx.ds.metricFindQuery(query, {}).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
it('should return list of all column values', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, {});
|
||||
|
||||
it('should return list of all column values', () => {
|
||||
expect(datasourceRequestMock).toBeCalledTimes(1);
|
||||
expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'");
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'");
|
||||
expect(results.length).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery with key, value columns', () => {
|
||||
let results: any;
|
||||
const query = 'select * from atable';
|
||||
const response = {
|
||||
results: {
|
||||
@ -239,14 +211,10 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 }));
|
||||
ctx.ds.metricFindQuery(query).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
it('should return list of as text, value', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, {});
|
||||
|
||||
it('should return list of as text, value', () => {
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('value1');
|
||||
@ -256,7 +224,6 @@ describe('MySQLDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
|
||||
let results: any;
|
||||
const query = 'select * from atable';
|
||||
const response = {
|
||||
results: {
|
||||
@ -279,14 +246,10 @@ describe('MySQLDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 }));
|
||||
ctx.ds.metricFindQuery(query).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
it('should return list of unique keys', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, {});
|
||||
|
||||
it('should return list of unique keys', () => {
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].text).toBe('aTitle');
|
||||
expect(results[0].value).toBe('same');
|
||||
@ -294,52 +257,55 @@ describe('MySQLDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When interpolating variables', () => {
|
||||
beforeEach(() => {
|
||||
ctx.variable = { ...initialCustomVariableModelState };
|
||||
});
|
||||
|
||||
describe('and value is a string', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc');
|
||||
const { ds, variable } = setupTextContext({});
|
||||
expect(ds.interpolateVariable('abc', variable)).toEqual('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is a number', () => {
|
||||
it('should return an unquoted value', () => {
|
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000);
|
||||
const { ds, variable } = setupTextContext({});
|
||||
expect(ds.interpolateVariable(1000, variable)).toEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and value is an array of strings', () => {
|
||||
it('should return comma separated quoted values', () => {
|
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'");
|
||||
const { ds, variable } = setupTextContext({});
|
||||
expect(ds.interpolateVariable(['a', 'b', 'c'], variable)).toEqual("'a','b','c'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows multi-value and value is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.multi = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
const { ds, variable } = setupTextContext({});
|
||||
variable.multi = true;
|
||||
expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable contains single quote', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.multi = true;
|
||||
expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'");
|
||||
const { ds, variable } = setupTextContext({});
|
||||
variable.multi = true;
|
||||
expect(ds.interpolateVariable("a'bc", variable)).toEqual("'a''bc'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('and variable allows all and value is a string', () => {
|
||||
it('should return a quoted value', () => {
|
||||
ctx.variable.includeAll = true;
|
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
|
||||
const { ds, variable } = setupTextContext({});
|
||||
variable.includeAll = true;
|
||||
expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('targetContainsTemplate', () => {
|
||||
it('given query that contains template variable it should return true', () => {
|
||||
const { ds, templateSrv } = setupTextContext({});
|
||||
const rawSql = `SELECT
|
||||
$__timeGroup(createdAt,'$summarize') as time_sec,
|
||||
avg(value) as value,
|
||||
@ -360,10 +326,11 @@ describe('MySQLDatasource', () => {
|
||||
{ type: 'query', name: 'summarize', current: { value: '1m' } },
|
||||
{ type: 'query', name: 'host', current: { value: 'a' } },
|
||||
]);
|
||||
expect(ctx.ds.targetContainsTemplate(query)).toBeTruthy();
|
||||
expect(ds.targetContainsTemplate(query)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('given query that only contains global template variable it should return false', () => {
|
||||
const { ds, templateSrv } = setupTextContext({});
|
||||
const rawSql = `SELECT
|
||||
$__timeGroup(createdAt,'$__interval') as time_sec,
|
||||
avg(value) as value,
|
||||
@ -383,7 +350,19 @@ describe('MySQLDatasource', () => {
|
||||
{ type: 'query', name: 'summarize', current: { value: '1m' } },
|
||||
{ type: 'query', name: 'host', current: { value: 'a' } },
|
||||
]);
|
||||
expect(ctx.ds.targetContainsTemplate(query)).toBeFalsy();
|
||||
expect(ds.targetContainsTemplate(query)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createFetchResponse = <T>(data: T): FetchResponse<T> => ({
|
||||
data,
|
||||
status: 200,
|
||||
url: 'http://localhost:3000/api/query',
|
||||
config: { url: 'http://localhost:3000/api/query' },
|
||||
type: 'basic',
|
||||
statusText: 'Ok',
|
||||
redirected: false,
|
||||
headers: ({} as unknown) as Headers,
|
||||
ok: true,
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
|
||||
export interface MysqlQueryForInterpolation {
|
||||
alias?: any;
|
||||
format?: any;
|
||||
@ -5,3 +7,7 @@ export interface MysqlQueryForInterpolation {
|
||||
refId?: any;
|
||||
hide?: any;
|
||||
}
|
||||
|
||||
export interface MysqlMetricFindValue extends MetricFindValue {
|
||||
value?: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user