mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SQL data sources: Convert to return data frames (#32257)
Convert SQL data sources to return data frames. Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Will Browne <will.browne@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
This commit is contained in:
@@ -1,32 +1,34 @@
|
||||
import { map as _map, filter } from 'lodash';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map as _map } from 'lodash';
|
||||
import { of } from 'rxjs';
|
||||
import { catchError, map, mapTo } from 'rxjs/operators';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query';
|
||||
import ResponseParser, { MysqlResponse } from './response_parser';
|
||||
import { MysqlMetricFindValue, MysqlQueryForInterpolation } from './types';
|
||||
import { getBackendSrv, DataSourceWithBackend, FetchResponse, BackendDataSourceResponse } from '@grafana/runtime';
|
||||
import { DataSourceInstanceSettings, ScopedVars, MetricFindValue, AnnotationEvent } from '@grafana/data';
|
||||
import MySQLQueryModel from 'app/plugins/datasource/mysql/mysql_query_model';
|
||||
import ResponseParser from './response_parser';
|
||||
import { MysqlQueryForInterpolation, MySQLOptions, MySQLQuery } from './types';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
export class MysqlDatasource {
|
||||
export class MysqlDatasource extends DataSourceWithBackend<MySQLQuery, MySQLOptions> {
|
||||
id: any;
|
||||
name: any;
|
||||
responseParser: ResponseParser;
|
||||
queryModel: MysqlQuery;
|
||||
queryModel: MySQLQueryModel;
|
||||
interval: string;
|
||||
|
||||
constructor(
|
||||
instanceSettings: any,
|
||||
instanceSettings: DataSourceInstanceSettings<MySQLOptions>,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv(),
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.name = instanceSettings.name;
|
||||
this.id = instanceSettings.id;
|
||||
this.responseParser = new ResponseParser();
|
||||
this.queryModel = new MysqlQuery({});
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval || '1m';
|
||||
this.queryModel = new MySQLQueryModel({});
|
||||
const settingsData = instanceSettings.jsonData || ({} as MySQLOptions);
|
||||
this.interval = settingsData.timeInterval || '1m';
|
||||
}
|
||||
|
||||
interpolateVariable = (value: string | string[] | number, variable: any) => {
|
||||
@@ -68,40 +70,24 @@ export class MysqlDatasource {
|
||||
return expandedQueries;
|
||||
}
|
||||
|
||||
query(options: any): Observable<MysqlResponse> {
|
||||
const queries = filter(options.targets, (target) => {
|
||||
return target.hide !== true;
|
||||
}).map((target) => {
|
||||
const queryModel = new MysqlQuery(target, this.templateSrv, options.scopedVars);
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
rawSql: queryModel.render(this.interpolateVariable as any),
|
||||
format: target.format,
|
||||
};
|
||||
});
|
||||
|
||||
if (queries.length === 0) {
|
||||
return of({ data: [] });
|
||||
filterQuery(query: MySQLQuery): boolean {
|
||||
if (query.hide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: queries,
|
||||
},
|
||||
})
|
||||
.pipe(map(this.responseParser.processQueryResult));
|
||||
return true;
|
||||
}
|
||||
|
||||
annotationQuery(options: any) {
|
||||
applyTemplateVariables(target: MySQLQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||
const queryModel = new MySQLQueryModel(target, this.templateSrv, scopedVars);
|
||||
return {
|
||||
refId: target.refId,
|
||||
datasourceId: this.id,
|
||||
rawSql: queryModel.render(this.interpolateVariable as any),
|
||||
format: target.format,
|
||||
};
|
||||
}
|
||||
|
||||
async annotationQuery(options: any): Promise<AnnotationEvent[]> {
|
||||
if (!options.annotation.rawQuery) {
|
||||
return Promise.reject({
|
||||
message: 'Query missing in annotation definition',
|
||||
@@ -116,20 +102,26 @@ export class MysqlDatasource {
|
||||
};
|
||||
|
||||
return getBackendSrv()
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
.fetch<BackendDataSourceResponse>({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: [query],
|
||||
},
|
||||
requestId: options.annotation.name,
|
||||
})
|
||||
.pipe(map((data: any) => this.responseParser.transformAnnotationResponse(options, data)))
|
||||
.pipe(
|
||||
map(
|
||||
async (res: FetchResponse<BackendDataSourceResponse>) =>
|
||||
await this.responseParser.transformAnnotationResponse(options, res.data)
|
||||
)
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, optionalOptions: any): Promise<MysqlMetricFindValue[]> {
|
||||
metricFindQuery(query: string, optionalOptions: any): Promise<MetricFindValue[]> {
|
||||
let refId = 'tempvar';
|
||||
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
|
||||
refId = optionalOptions.variable.name;
|
||||
@@ -149,33 +141,30 @@ export class MysqlDatasource {
|
||||
};
|
||||
|
||||
const range = this.timeSrv.timeRange();
|
||||
const data = {
|
||||
queries: [interpolatedQuery],
|
||||
from: range.from.valueOf().toString(),
|
||||
to: range.to.valueOf().toString(),
|
||||
};
|
||||
|
||||
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
|
||||
data['from'] = optionalOptions.range.from.valueOf().toString();
|
||||
}
|
||||
if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
|
||||
data['to'] = optionalOptions.range.to.valueOf().toString();
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
.fetch<BackendDataSourceResponse>({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
data: {
|
||||
from: range.from.valueOf().toString(),
|
||||
to: range.to.valueOf().toString(),
|
||||
queries: [interpolatedQuery],
|
||||
},
|
||||
requestId: refId,
|
||||
})
|
||||
.pipe(map((data: any) => this.responseParser.parseMetricFindQueryResult(refId, data)))
|
||||
.pipe(
|
||||
map((rsp) => {
|
||||
return this.responseParser.transformMetricFindResponse(rsp);
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
testDatasource(): Promise<any> {
|
||||
return getBackendSrv()
|
||||
.fetch({
|
||||
url: '/api/tsdb/query',
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from: '5m',
|
||||
@@ -212,7 +201,7 @@ export class MysqlDatasource {
|
||||
if (target.rawQuery) {
|
||||
rawSql = target.rawSql;
|
||||
} else {
|
||||
const query = new MysqlQuery(target);
|
||||
const query = new MySQLQueryModel(target);
|
||||
rawSql = query.buildQuery();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
createResetHandler,
|
||||
PasswordFieldEnum,
|
||||
} from '../../../features/datasources/utils/passwordHandlers';
|
||||
import { MySQLQuery } from './types';
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
|
||||
class MysqlConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
@@ -31,10 +33,11 @@ const defaultQuery = `SELECT
|
||||
class MysqlAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
|
||||
annotation: any;
|
||||
declare annotation: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
constructor($scope: any) {
|
||||
this.annotation = $scope.ctrl.annotation;
|
||||
this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery;
|
||||
}
|
||||
}
|
||||
@@ -46,3 +49,8 @@ export {
|
||||
MysqlConfigCtrl as ConfigCtrl,
|
||||
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
export const plugin = new DataSourcePlugin<MysqlDatasource, MySQLQuery>(MysqlDatasource)
|
||||
.setQueryCtrl(MysqlQueryCtrl)
|
||||
.setConfigCtrl(MysqlConfigCtrl)
|
||||
.setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { find, map } from 'lodash';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
|
||||
export default class MysqlQuery {
|
||||
export default class MySQLQueryModel {
|
||||
target: any;
|
||||
templateSrv: any;
|
||||
scopedVars: any;
|
||||
@@ -3,7 +3,7 @@ import appEvents from 'app/core/app_events';
|
||||
import { MysqlMetaQuery } from './meta_query';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { SqlPart } from 'app/core/components/sql_part/sql_part';
|
||||
import MysqlQuery from './mysql_query';
|
||||
import MySQLQueryModel from './mysql_query_model';
|
||||
import sqlPart from './sql_part';
|
||||
import { auto } from 'angular';
|
||||
import { PanelEvents, QueryResultMeta } from '@grafana/data';
|
||||
@@ -27,7 +27,7 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
lastQueryError?: string;
|
||||
showHelp!: boolean;
|
||||
|
||||
queryModel: MysqlQuery;
|
||||
queryModel: MySQLQueryModel;
|
||||
metaBuilder: MysqlMetaQuery;
|
||||
lastQueryMeta?: QueryResultMeta;
|
||||
tableSegment: any;
|
||||
@@ -50,7 +50,7 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
super($scope, $injector);
|
||||
|
||||
this.target = this.target;
|
||||
this.queryModel = new MysqlQuery(this.target, templateSrv, this.panel.scopedVars);
|
||||
this.queryModel = new MySQLQueryModel(this.target, templateSrv, this.panel.scopedVars);
|
||||
this.metaBuilder = new MysqlMetaQuery(this.target, this.queryModel);
|
||||
this.updateProjection();
|
||||
|
||||
|
||||
@@ -1,91 +1,57 @@
|
||||
import { map } 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>;
|
||||
}
|
||||
import { AnnotationEvent, DataFrame, FieldType, MetricFindValue } from '@grafana/data';
|
||||
import { BackendDataSourceResponse, FetchResponse, toDataQueryResponse } from '@grafana/runtime';
|
||||
|
||||
export default class ResponseParser {
|
||||
processQueryResult(res: any): MysqlResponse {
|
||||
const data: any[] = [];
|
||||
transformMetricFindResponse(raw: FetchResponse<BackendDataSourceResponse>): MetricFindValue[] {
|
||||
const frames = toDataQueryResponse(raw).data as DataFrame[];
|
||||
|
||||
if (!res.data.results) {
|
||||
return { data: data };
|
||||
}
|
||||
|
||||
for (const key in res.data.results) {
|
||||
const queryRes = res.data.results[key];
|
||||
|
||||
if (queryRes.series) {
|
||||
for (const series of queryRes.series) {
|
||||
data.push({
|
||||
target: series.name,
|
||||
datapoints: series.points,
|
||||
refId: queryRes.refId,
|
||||
meta: queryRes.meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (queryRes.tables) {
|
||||
for (const table of queryRes.tables) {
|
||||
table.type = 'table';
|
||||
table.refId = queryRes.refId;
|
||||
table.meta = queryRes.meta;
|
||||
data.push(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data };
|
||||
}
|
||||
|
||||
parseMetricFindQueryResult(refId: string, results: any): MysqlMetricFindValue[] {
|
||||
if (!results || results.data.length === 0 || results.data.results[refId].meta.rowCount === 0) {
|
||||
if (!frames || !frames.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columns = results.data.results[refId].tables[0].columns;
|
||||
const rows = results.data.results[refId].tables[0].rows;
|
||||
const textColIndex = this.findColIndex(columns, '__text');
|
||||
const valueColIndex = this.findColIndex(columns, '__value');
|
||||
const frame = frames[0];
|
||||
|
||||
if (columns.length === 2 && textColIndex !== -1 && valueColIndex !== -1) {
|
||||
return this.transformToKeyValueList(rows, textColIndex, valueColIndex);
|
||||
const values: MetricFindValue[] = [];
|
||||
const textField = frame.fields.find((f) => f.name === '__text');
|
||||
const valueField = frame.fields.find((f) => f.name === '__value');
|
||||
|
||||
if (textField && valueField) {
|
||||
for (let i = 0; i < textField.values.length; i++) {
|
||||
values.push({ text: '' + textField.values.get(i), value: '' + valueField.values.get(i) });
|
||||
}
|
||||
} else {
|
||||
const textFields = frame.fields.filter((f) => f.type === FieldType.string);
|
||||
if (textFields) {
|
||||
values.push(
|
||||
...textFields
|
||||
.flatMap((f) => f.values.toArray())
|
||||
.map((v) => ({
|
||||
text: '' + v,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.transformToSimpleList(rows);
|
||||
return Array.from(new Set(values.map((v) => v.text))).map((text) => ({
|
||||
text,
|
||||
value: values.find((v) => v.text === text)?.value,
|
||||
}));
|
||||
}
|
||||
|
||||
transformToKeyValueList(rows: any, textColIndex: number, valueColIndex: number) {
|
||||
transformToKeyValueList(rows: any, textColIndex: number, valueColIndex: number): MetricFindValue[] {
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (!this.containsKey(res, rows[i][textColIndex])) {
|
||||
res.push({
|
||||
text: rows[i][textColIndex],
|
||||
value: rows[i][valueColIndex],
|
||||
});
|
||||
res.push({ text: rows[i][textColIndex], value: rows[i][valueColIndex] });
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
transformToSimpleList(rows: any) {
|
||||
transformToSimpleList(rows: any): MetricFindValue[] {
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -120,47 +86,38 @@ export default class ResponseParser {
|
||||
return false;
|
||||
}
|
||||
|
||||
transformAnnotationResponse(options: any, data: any) {
|
||||
const table = data.data.results[options.annotation.name].tables[0];
|
||||
async transformAnnotationResponse(options: any, data: BackendDataSourceResponse): Promise<AnnotationEvent[]> {
|
||||
const frames = toDataQueryResponse({ data: data }).data as DataFrame[];
|
||||
const frame = frames[0];
|
||||
const timeField = frame.fields.find((f) => f.name === 'time' || f.name === 'time_sec');
|
||||
|
||||
let timeColumnIndex = -1;
|
||||
let timeEndColumnIndex = -1;
|
||||
let textColumnIndex = -1;
|
||||
let tagsColumnIndex = -1;
|
||||
|
||||
for (let i = 0; i < table.columns.length; i++) {
|
||||
if (table.columns[i].text === 'time_sec' || table.columns[i].text === 'time') {
|
||||
timeColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'timeend') {
|
||||
timeEndColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'title') {
|
||||
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') {
|
||||
tagsColumnIndex = i;
|
||||
}
|
||||
if (!timeField) {
|
||||
throw new Error('Missing mandatory time column (with time column alias) in annotation query');
|
||||
}
|
||||
|
||||
if (timeColumnIndex === -1) {
|
||||
throw {
|
||||
message: 'Missing mandatory time column (with time_sec column alias) in annotation query.',
|
||||
};
|
||||
if (frame.fields.find((f) => f.name === 'title')) {
|
||||
throw new Error('The title column for annotations is deprecated, now only a column named text is returned');
|
||||
}
|
||||
|
||||
const list = [];
|
||||
for (let i = 0; i < table.rows.length; i++) {
|
||||
const row = table.rows[i];
|
||||
const timeEnd =
|
||||
timeEndColumnIndex !== -1 && row[timeEndColumnIndex] ? Math.floor(row[timeEndColumnIndex]) : undefined;
|
||||
const timeEndField = frame.fields.find((f) => f.name === 'timeend');
|
||||
const textField = frame.fields.find((f) => f.name === 'text');
|
||||
const tagsField = frame.fields.find((f) => f.name === 'tags');
|
||||
|
||||
const list: AnnotationEvent[] = [];
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const timeEnd = timeEndField && timeEndField.values.get(i) ? Math.floor(timeEndField.values.get(i)) : undefined;
|
||||
list.push({
|
||||
annotation: options.annotation,
|
||||
time: Math.floor(row[timeColumnIndex]),
|
||||
time: Math.floor(timeField.values.get(i)),
|
||||
timeEnd,
|
||||
text: row[textColumnIndex] ? row[textColumnIndex].toString() : '',
|
||||
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [],
|
||||
text: textField && textField.values.get(i) ? textField.values.get(i) : '',
|
||||
tags:
|
||||
tagsField && tagsField.values.get(i)
|
||||
? tagsField.values
|
||||
.get(i)
|
||||
.trim()
|
||||
.split(/\s*,\s*/)
|
||||
: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import { of } from 'rxjs';
|
||||
import { dateTime, toUtc } from '@grafana/data';
|
||||
import {
|
||||
dataFrameToJSON,
|
||||
DataQueryRequest,
|
||||
DataSourceInstanceSettings,
|
||||
dateTime,
|
||||
MutableDataFrame,
|
||||
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),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
import { FetchResponse, setBackendSrv } from '@grafana/runtime';
|
||||
import { MySQLOptions, MySQLQuery } from './../types';
|
||||
|
||||
describe('MySQLDatasource', () => {
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
const setupTextContext = (response: any) => {
|
||||
const instanceSettings = { name: 'mysql' };
|
||||
jest.clearAllMocks();
|
||||
setBackendSrv(backendSrv);
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
const instanceSettings = ({
|
||||
jsonData: {
|
||||
defaultProject: 'testproject',
|
||||
},
|
||||
} as unknown) as DataSourceInstanceSettings<MySQLOptions>;
|
||||
const templateSrv: TemplateSrv = new TemplateSrv();
|
||||
const variable = { ...initialCustomVariableModelState };
|
||||
const raw = {
|
||||
from: toUtc('2018-04-25 10:00'),
|
||||
to: toUtc('2018-04-25 11:00'),
|
||||
@@ -28,19 +38,44 @@ describe('MySQLDatasource', () => {
|
||||
raw: raw,
|
||||
}),
|
||||
};
|
||||
const variable = { ...initialCustomVariableModelState };
|
||||
|
||||
jest.clearAllMocks();
|
||||
fetchMock.mockImplementation((options) => of(createFetchResponse(response)));
|
||||
|
||||
const ds = new MysqlDatasource(instanceSettings, templateSrv, timeSrvMock);
|
||||
|
||||
return { ds, variable, templateSrv };
|
||||
return { ds, variable, templateSrv, fetchMock };
|
||||
};
|
||||
|
||||
describe('When performing annotationQuery', () => {
|
||||
const annotationName = 'MyAnno';
|
||||
describe('When performing a query with hidden target', () => {
|
||||
it('should return empty result and backendSrv.fetch should not be called', async () => {
|
||||
const options = ({
|
||||
range: {
|
||||
from: dateTime(1432288354),
|
||||
to: dateTime(1432288401),
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
format: 'table',
|
||||
rawQuery: true,
|
||||
rawSql: 'select time, metric, value from grafana_metric',
|
||||
refId: 'A',
|
||||
datasource: 'gdev-ds',
|
||||
hide: true,
|
||||
},
|
||||
],
|
||||
} as unknown) as DataQueryRequest<MySQLQuery>;
|
||||
|
||||
const { ds, fetchMock } = setupTextContext({});
|
||||
|
||||
await expect(ds.query(options)).toEmitValuesWith((received) => {
|
||||
expect(received[0]).toEqual({ data: [] });
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing annotationQuery', () => {
|
||||
let results: any;
|
||||
const annotationName = 'MyAnno';
|
||||
const options = {
|
||||
annotation: {
|
||||
name: annotationName,
|
||||
@@ -51,38 +86,37 @@ describe('MySQLDatasource', () => {
|
||||
to: dateTime(1432288401),
|
||||
},
|
||||
};
|
||||
|
||||
const response = {
|
||||
results: {
|
||||
MyAnno: {
|
||||
refId: annotationName,
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: 'time_sec' }, { text: 'text' }, { text: 'tags' }],
|
||||
rows: [
|
||||
[1432288355, 'some text', 'TagA,TagB'],
|
||||
[1432288390, 'some text2', ' TagB , TagC'],
|
||||
[1432288400, 'some text3'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time_sec', values: [1432288355, 1432288390, 1432288400] },
|
||||
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
|
||||
{ name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] },
|
||||
],
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return annotation list', async () => {
|
||||
beforeEach(async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const results = await ds.annotationQuery(options);
|
||||
const data = await ds.annotationQuery(options);
|
||||
results = data;
|
||||
});
|
||||
|
||||
it('should return annotation list', async () => {
|
||||
expect(results.length).toBe(3);
|
||||
|
||||
expect(results[0].text).toBe('some text');
|
||||
expect(results[0].tags[0]).toBe('TagA');
|
||||
expect(results[0].tags[1]).toBe('TagB');
|
||||
|
||||
expect(results[1].tags[0]).toBe('TagB');
|
||||
expect(results[1].tags[1]).toBe('TagC');
|
||||
|
||||
expect(results[2].tags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -92,19 +126,19 @@ describe('MySQLDatasource', () => {
|
||||
const response = {
|
||||
results: {
|
||||
tempvar: {
|
||||
meta: {
|
||||
rowCount: 3,
|
||||
},
|
||||
refId: 'tempvar',
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: 'title' }, { text: 'text' }],
|
||||
rows: [
|
||||
['aTitle', 'some text'],
|
||||
['aTitle2', 'some text2'],
|
||||
['aTitle3', 'some text3'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
|
||||
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
|
||||
],
|
||||
meta: {
|
||||
executedQueryString: 'select * from atable',
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -125,26 +159,26 @@ describe('MySQLDatasource', () => {
|
||||
const response = {
|
||||
results: {
|
||||
tempvar: {
|
||||
meta: {
|
||||
rowCount: 3,
|
||||
},
|
||||
refId: 'tempvar',
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: 'title' }, { text: 'text' }],
|
||||
rows: [
|
||||
['aTitle', 'some text'],
|
||||
['aTitle2', 'some text2'],
|
||||
['aTitle3', 'some text3'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
|
||||
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
|
||||
],
|
||||
meta: {
|
||||
executedQueryString: 'select * from atable',
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return list of all column values', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const { ds, fetchMock } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, { searchFilter: 'aTit' });
|
||||
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
@@ -160,26 +194,26 @@ describe('MySQLDatasource', () => {
|
||||
const response = {
|
||||
results: {
|
||||
tempvar: {
|
||||
meta: {
|
||||
rowCount: 3,
|
||||
},
|
||||
refId: 'tempvar',
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: 'title' }, { text: 'text' }],
|
||||
rows: [
|
||||
['aTitle', 'some text'],
|
||||
['aTitle2', 'some text2'],
|
||||
['aTitle3', 'some text3'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
|
||||
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
|
||||
],
|
||||
meta: {
|
||||
executedQueryString: 'select * from atable',
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return list of all column values', async () => {
|
||||
const { ds } = setupTextContext(response);
|
||||
const { ds, fetchMock } = setupTextContext(response);
|
||||
const results = await ds.metricFindQuery(query, {});
|
||||
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
@@ -193,19 +227,19 @@ describe('MySQLDatasource', () => {
|
||||
const response = {
|
||||
results: {
|
||||
tempvar: {
|
||||
meta: {
|
||||
rowCount: 3,
|
||||
},
|
||||
refId: 'tempvar',
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: '__value' }, { text: '__text' }],
|
||||
rows: [
|
||||
['value1', 'aTitle'],
|
||||
['value2', 'aTitle2'],
|
||||
['value3', 'aTitle3'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: '__value', values: ['value1', 'value2', 'value3'] },
|
||||
{ name: '__text', values: ['aTitle', 'aTitle2', 'aTitle3'] },
|
||||
],
|
||||
meta: {
|
||||
executedQueryString: 'select * from atable',
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -228,19 +262,19 @@ describe('MySQLDatasource', () => {
|
||||
const response = {
|
||||
results: {
|
||||
tempvar: {
|
||||
meta: {
|
||||
rowCount: 3,
|
||||
},
|
||||
refId: 'tempvar',
|
||||
tables: [
|
||||
{
|
||||
columns: [{ text: '__text' }, { text: '__value' }],
|
||||
rows: [
|
||||
['aTitle', 'same'],
|
||||
['aTitle', 'same'],
|
||||
['aTitle', 'diff'],
|
||||
],
|
||||
},
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: '__text', values: ['aTitle', 'aTitle', 'aTitle'] },
|
||||
{ name: '__value', values: ['same', 'same', 'diff'] },
|
||||
],
|
||||
meta: {
|
||||
executedQueryString: 'select * from atable',
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
|
||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||
export interface MysqlQueryForInterpolation {
|
||||
alias?: any;
|
||||
format?: any;
|
||||
rawSql?: any;
|
||||
refId?: any;
|
||||
refId: any;
|
||||
hide?: any;
|
||||
}
|
||||
|
||||
export interface MysqlMetricFindValue extends MetricFindValue {
|
||||
value?: string;
|
||||
export interface MySQLOptions extends DataSourceJsonData {
|
||||
timeInterval: string;
|
||||
}
|
||||
|
||||
export type ResultFormat = 'time_series' | 'table';
|
||||
|
||||
export interface MySQLQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: ResultFormat;
|
||||
rawSql?: any;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user