MSSQL: Migrate to React (#51765)

* Fix: sql plugins feature

* SQLDS: Use builtin annotation editor

Plus strict rule fixes

* MSSQL: Migrate query editor to React

* Make code editor work

* Make SQLOptions and SQLQuery in SQLDatasource and in Editor generic

* MSSQL: Fix ts issues

* Fix SQLDatasource refID

* Remove comment

* Revert "Make SQLOptions and SQLQuery in SQLDatasource and in Editor generic"

This reverts commit 1d15b4061a.

* Fix ts issues without generic

* TS
pull/50343/head
Zoltán Bedi 3 years ago committed by GitHub
parent 7b40322bbe
commit 35d98104ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      .betterer.results
  2. 2
      public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx
  3. 5
      public/app/features/plugins/sql/components/configuration/types.ts
  4. 22
      public/app/features/plugins/sql/datasource/SqlDatasource.ts
  5. 25
      public/app/features/plugins/sql/types.ts
  6. 15
      public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts
  7. 25
      public/app/plugins/datasource/mssql/MSSqlQueryModel.ts
  8. 125
      public/app/plugins/datasource/mssql/datasource.test.ts
  9. 252
      public/app/plugins/datasource/mssql/datasource.ts
  10. 35
      public/app/plugins/datasource/mssql/module.ts
  11. 90
      public/app/plugins/datasource/mssql/partials/query.editor.html
  12. 65
      public/app/plugins/datasource/mssql/query_ctrl.ts
  13. 53
      public/app/plugins/datasource/mssql/response_parser.ts
  14. 136
      public/app/plugins/datasource/mssql/sqlCompletionProvider.ts
  15. 131
      public/app/plugins/datasource/mssql/sqlUtil.ts
  16. 34
      public/app/plugins/datasource/mssql/types.ts
  17. 2
      public/app/plugins/datasource/mysql/types.ts
  18. 2
      public/app/plugins/datasource/postgres/types.ts

@ -7778,52 +7778,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mssql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
],
"public/app/plugins/datasource/mssql/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mssql/query_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/datasource/mssql/response_parser.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mssql/specs/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
],
"public/app/plugins/datasource/mssql/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/datasource/mysql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

@ -3,7 +3,7 @@ import React from 'react';
import { FieldSet, InlineField } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { SQLConnectionLimits } from './types';
import { SQLConnectionLimits } from '../../types';
interface Props<T> {
onPropertyChanged: (property: keyof T, value?: number) => void;

@ -1,5 +0,0 @@
export interface SQLConnectionLimits {
maxOpenConns: number;
maxIdleConns: number;
connMaxLifetime: number;
}

@ -25,15 +25,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { VariableWithMultiSupport } from '../../../variables/types';
import { getSearchFilterScopedVar, SearchFilterOptions } from '../../../variables/utils';
import { MACRO_NAMES } from '../constants';
import {
DB,
SQLQuery,
SQLOptions,
SqlQueryForInterpolation,
ResponseParser,
SqlQueryModel,
QueryFormat,
} from '../types';
import { DB, SQLQuery, SQLOptions, ResponseParser, SqlQueryModel, QueryFormat } from '../types';
export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLOptions> {
id: number;
@ -82,10 +74,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
return value;
};
interpolateVariablesInQueries(
queries: SqlQueryForInterpolation[],
scopedVars: ScopedVars
): SqlQueryForInterpolation[] {
interpolateVariablesInQueries(queries: SQLQuery[], scopedVars: ScopedVars): SQLQuery[] {
let expandedQueries = queries;
if (queries && queries.length > 0) {
expandedQueries = queries.map((query) => {
@ -141,8 +130,8 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
return this.getResponseParser().transformMetricFindResponse(response);
}
async runSql<T>(query: string, options?: MetricFindQueryOptions) {
const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table }, options);
async runSql<T>(query: string, options?: RunSQLOptions) {
const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table, refId: options?.refId }, options);
return new DataFrameView<T>(frame);
}
@ -212,6 +201,9 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
}
}
interface RunSQLOptions extends MetricFindQueryOptions {
refId?: string;
}
interface MetricFindQueryOptions extends SearchFilterOptions {
range?: TimeRange;
}

@ -18,18 +18,21 @@ import {
QueryEditorPropertyExpression,
} from './expressions';
export interface SqlQueryForInterpolation {
dataset?: string;
alias?: string;
format?: QueryFormat;
rawSql?: string;
refId: string;
hide?: boolean;
}
export interface SQLOptions extends DataSourceJsonData {
timeInterval: string;
export interface SQLConnectionLimits {
maxOpenConns: number;
maxIdleConns: number;
connMaxLifetime: number;
}
export interface SQLOptions extends SQLConnectionLimits, DataSourceJsonData {
tlsAuth: boolean;
tlsAuthWithCACert: boolean;
timezone: string;
tlsSkipVerify: boolean;
user: string;
database: string;
url: string;
timeInterval: string;
}
export enum QueryFormat {

@ -0,0 +1,15 @@
export function showDatabases() {
// Return only user defined databases
return `SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb');`;
}
export function showTables(dataset?: string) {
return `SELECT TABLE_NAME as name
FROM [${dataset}].INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'`;
}
export function getSchema(table?: string) {
return `SELECT COLUMN_NAME as 'column',DATA_TYPE as 'type'
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='${table}';`;
}

@ -0,0 +1,25 @@
import { ScopedVars } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { applyQueryDefaults } from 'app/features/plugins/sql/defaults';
import { SQLQuery, SqlQueryModel } from 'app/features/plugins/sql/types';
import { FormatRegistryID } from 'app/features/templating/formatRegistry';
export class MSSqlQueryModel implements SqlQueryModel {
target: SQLQuery;
templateSrv?: TemplateSrv;
scopedVars?: ScopedVars;
constructor(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.target = applyQueryDefaults(target || { refId: 'A' });
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
}
interpolate() {
return this.templateSrv?.replace(this.target.rawSql, this.scopedVars, FormatRegistryID.sqlString) || '';
}
quoteLiteral(value: string) {
return "'" + value.replace(/'/g, "''") + "'";
}
}

@ -1,85 +1,48 @@
import { of } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { dataFrameToJSON, dateTime, MetricFindValue, MutableDataFrame } from '@grafana/data';
import {
dataFrameToJSON,
DataSourceInstanceSettings,
dateTime,
MetricFindValue,
MutableDataFrame,
TimeRange,
} from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import { SQLQuery } from 'app/features/plugins/sql/types';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer';
import { MssqlDatasource } from '../datasource';
import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer';
import { MssqlDatasource } from './datasource';
import { MssqlOptions } from './types';
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
const instanceSettings = {
id: 1,
uid: 'mssql-datasource',
type: 'mssql',
name: 'MSSQL',
access: 'direct',
} as DataSourceInstanceSettings<MssqlOptions>;
describe('MSSQLDatasource', () => {
const templateSrv: TemplateSrv = new TemplateSrv();
const fetchMock = jest.spyOn(backendSrv, 'fetch');
const ctx: any = {};
const ctx = {
ds: new MssqlDatasource(instanceSettings),
variable: { ...initialCustomVariableModelState },
};
beforeEach(() => {
jest.clearAllMocks();
ctx.instanceSettings = { name: 'mssql' };
ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv);
});
describe('When performing annotationQuery', () => {
let results: any;
const annotationName = 'MyAnno';
const options = {
annotation: {
name: annotationName,
rawQuery: 'select time, text, tags from table;',
},
range: {
from: dateTime(1432288354),
to: dateTime(1432288401),
},
};
const response = {
results: {
MyAnno: {
frames: [
dataFrameToJSON(
new MutableDataFrame({
fields: [
{ name: 'time', values: [1521545610656, 1521546251185, 1521546501378] },
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
{ name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] },
],
})
),
],
},
},
};
beforeEach(() => {
fetchMock.mockImplementation(() => of(createFetchResponse(response)));
return 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');
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);
});
ctx.ds = new MssqlDatasource(instanceSettings);
});
describe('When performing metricFindQuery that returns multiple string fields', () => {
@ -118,7 +81,7 @@ describe('MSSQLDatasource', () => {
});
describe('When performing metricFindQuery with key, value columns', () => {
let results: any;
let results: MetricFindValue[];
const query = 'select * from atable';
const response = {
results: {
@ -140,7 +103,7 @@ describe('MSSQLDatasource', () => {
beforeEach(() => {
fetchMock.mockImplementation(() => of(createFetchResponse(response)));
return ctx.ds.metricFindQuery(query).then((data: any) => {
return ctx.ds.metricFindQuery(query).then((data) => {
results = data;
});
});
@ -155,7 +118,7 @@ describe('MSSQLDatasource', () => {
});
describe('When performing metricFindQuery without key, value columns', () => {
let results: any;
let results: MetricFindValue[];
const query = 'select id, values from atable';
const response = {
results: {
@ -181,7 +144,7 @@ describe('MSSQLDatasource', () => {
beforeEach(() => {
fetchMock.mockImplementation(() => of(createFetchResponse(response)));
return ctx.ds.metricFindQuery(query).then((data: any) => {
return ctx.ds.metricFindQuery(query).then((data) => {
results = data;
});
});
@ -199,7 +162,7 @@ describe('MSSQLDatasource', () => {
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
let results: any;
let results: MetricFindValue[];
const query = 'select * from atable';
const response = {
results: {
@ -220,7 +183,7 @@ describe('MSSQLDatasource', () => {
beforeEach(() => {
fetchMock.mockImplementation(() => of(createFetchResponse(response)));
return ctx.ds.metricFindQuery(query).then((data: any) => {
return ctx.ds.metricFindQuery(query).then((data) => {
results = data;
});
});
@ -247,9 +210,10 @@ describe('MSSQLDatasource', () => {
},
},
};
const time = {
const time: TimeRange = {
from: dateTime(1521545610656),
to: dateTime(1521546251185),
raw: { from: '1521545610656', to: '1521546251185' },
};
beforeEach(() => {
@ -268,10 +232,6 @@ describe('MSSQLDatasource', () => {
});
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');
@ -314,6 +274,7 @@ describe('MSSQLDatasource', () => {
describe('targetContainsTemplate', () => {
it('given query that contains template variable it should return true', () => {
const templateSrv = new TemplateSrv();
const rawSql = `SELECT
$__timeGroup(createdAt,'$summarize') as time,
avg(value) as value,
@ -326,17 +287,21 @@ describe('MSSQLDatasource', () => {
hostname IN($host)
GROUP BY $__timeGroup(createdAt,'$summarize'), hostname
ORDER BY 1`;
const query = {
const query: SQLQuery = {
rawSql,
refId: 'A',
};
templateSrv.init([
{ type: 'query', name: 'summarize', current: { value: '1m' } },
{ type: 'query', name: 'host', current: { value: 'a' } },
]);
expect(ctx.ds.targetContainsTemplate(query)).toBeTruthy();
const ds = new MssqlDatasource(instanceSettings, templateSrv);
expect(ds.targetContainsTemplate(query)).toBeTruthy();
});
it('given query that only contains global template variable it should return false', () => {
const templateSrv: TemplateSrv = new TemplateSrv();
const rawSql = `SELECT
$__timeGroup(createdAt,'$__interval') as time,
avg(value) as value,
@ -348,14 +313,16 @@ describe('MSSQLDatasource', () => {
measurement = 'logins.count'
GROUP BY $__timeGroup(createdAt,'$summarize'), hostname
ORDER BY 1`;
const query = {
const query: SQLQuery = {
rawSql,
refId: 'A',
};
templateSrv.init([
{ type: 'query', name: 'summarize', current: { value: '1m' } },
{ type: 'query', name: 'host', current: { value: 'a' } },
]);
expect(ctx.ds.targetContainsTemplate(query)).toBeFalsy();
const ds = new MssqlDatasource(instanceSettings, templateSrv);
expect(ds.targetContainsTemplate(query)).toBeFalsy();
});
});
});

@ -1,191 +1,97 @@
import { map as _map } from 'lodash';
import { lastValueFrom, of } from 'rxjs';
import { catchError, map, mapTo } from 'rxjs/operators';
import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars, TimeRange } from '@grafana/data';
import { BackendDataSourceResponse, DataSourceWithBackend, FetchResponse, getBackendSrv } from '@grafana/runtime';
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import ResponseParser from './response_parser';
import { MssqlOptions, MssqlQuery, MssqlQueryForInterpolation } from './types';
export class MssqlDatasource extends DataSourceWithBackend<MssqlQuery, MssqlOptions> {
id: any;
name: any;
responseParser: ResponseParser;
interval: string;
constructor(
instanceSettings: DataSourceInstanceSettings<MssqlOptions>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser();
const settingsData = instanceSettings.jsonData || ({} as MssqlOptions);
this.interval = settingsData.timeInterval || '1m';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { LanguageCompletionProvider } from '@grafana/experimental';
import { TemplateSrv } from '@grafana/runtime';
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
import { DB, ResponseParser, SQLQuery, SQLSelectableValue } from 'app/features/plugins/sql/types';
import { getSchema, showDatabases, showTables } from './MSSqlMetaQuery';
import { MSSqlQueryModel } from './MSSqlQueryModel';
import { MSSqlResponseParser } from './response_parser';
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
import { getIcon, getRAQBType, SCHEMA_NAME, toRawSql } from './sqlUtil';
import { MssqlOptions } from './types';
export class MssqlDatasource extends SqlDatasource {
completionProvider: LanguageCompletionProvider | undefined = undefined;
constructor(instanceSettings: DataSourceInstanceSettings<MssqlOptions>, templateSrv?: TemplateSrv) {
super(instanceSettings, templateSrv);
}
interpolateVariable(value: any, variable: any) {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return "'" + value.replace(/'/g, `''`) + "'";
} else {
return value;
}
}
if (typeof value === 'number') {
return value;
}
const quotedValues = _map(value, (val) => {
if (typeof value === 'number') {
return value;
}
return "'" + val.replace(/'/g, `''`) + "'";
});
return quotedValues.join(',');
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): MSSqlQueryModel {
return new MSSqlQueryModel(target, templateSrv, scopedVars);
}
interpolateVariablesInQueries(
queries: MssqlQueryForInterpolation[],
scopedVars: ScopedVars
): MssqlQueryForInterpolation[] {
let expandedQueries = queries;
if (queries && queries.length > 0) {
expandedQueries = queries.map((query) => {
const expandedQuery = {
...query,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true,
};
return expandedQuery;
});
}
return expandedQueries;
getResponseParser(): ResponseParser {
return new MSSqlResponseParser();
}
applyTemplateVariables(target: MssqlQuery, scopedVars: ScopedVars): Record<string, any> {
return {
refId: target.refId,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(target.rawSql, scopedVars, this.interpolateVariable),
format: target.format,
};
async fetchDatasets(): Promise<string[]> {
const datasets = await this.runSql<{ name: string[] }>(showDatabases(), { refId: 'datasets' });
return datasets.fields.name.values.toArray().flat();
}
async annotationQuery(options: any): Promise<AnnotationEvent[]> {
if (!options.annotation.rawQuery) {
return Promise.reject({ message: 'Query missing in annotation definition' });
}
const query = {
refId: options.annotation.name,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(options.annotation.rawQuery, options.scopedVars, this.interpolateVariable),
format: 'table',
};
return lastValueFrom(
getBackendSrv()
.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(
async (res: FetchResponse<BackendDataSourceResponse>) =>
await this.responseParser.transformAnnotationResponse(options, res.data)
)
)
);
async fetchTables(dataset?: string): Promise<string[]> {
const tables = await this.runSql<{ name: string[] }>(showTables(dataset), { refId: 'tables' });
return tables.fields.name.values.toArray().flat();
}
filterQuery(query: MssqlQuery): boolean {
return !query.hide;
async fetchFields(query: SQLQuery): Promise<SQLSelectableValue[]> {
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.table), { refId: 'columns' });
const result: SQLSelectableValue[] = [];
for (let i = 0; i < schema.length; i++) {
const column = schema.fields.column.values.get(i);
const type = schema.fields.type.values.get(i);
result.push({ label: column, value: column, type, icon: getIcon(type), raqbFieldType: getRAQBType(type) });
}
return result;
}
metricFindQuery(query: string, optionalOptions: any): Promise<MetricFindValue[]> {
let refId = 'tempvar';
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
refId = optionalOptions.variable.name;
getSqlCompletionProvider(db: DB): LanguageCompletionProvider {
if (this.completionProvider !== undefined) {
return this.completionProvider;
}
const range = optionalOptions?.range as TimeRange;
const interpolatedQuery = {
refId: refId,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable),
format: 'table',
const args = {
getColumns: { current: (query: SQLQuery) => fetchColumns(db, query) },
getTables: { current: (dataset?: string) => fetchTables(db, dataset) },
};
return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
data: {
from: range?.from?.valueOf()?.toString(),
to: range?.to?.valueOf()?.toString(),
queries: [interpolatedQuery],
},
requestId: refId,
})
.pipe(
map((rsp) => {
return this.responseParser.transformMetricFindResponse(rsp);
}),
catchError((err) => {
return of([]);
})
)
);
}
testDatasource(): Promise<any> {
return lastValueFrom(
getBackendSrv()
.fetch({
url: '/api/ds/query',
method: 'POST',
data: {
from: '5m',
to: 'now',
queries: [
{
refId: 'A',
intervalMs: 1,
maxDataPoints: 1,
datasource: this.getRef(),
rawSql: 'SELECT 1',
format: 'table',
},
],
},
})
.pipe(
mapTo({ status: 'success', message: 'Database Connection OK' }),
catchError((err) => {
return of(toTestingStatus(err));
})
)
);
this.completionProvider = getSqlCompletionProvider(args);
return this.completionProvider;
}
targetContainsTemplate(query: MssqlQuery): boolean {
const rawSql = query.rawSql.replace('$__', '');
return this.templateSrv.containsTemplate(rawSql);
getDB(): DB {
return {
init: () => Promise.resolve(true),
datasets: () => this.fetchDatasets(),
tables: (dataset?: string) => this.fetchTables(dataset),
getSqlCompletionProvider: () => this.getSqlCompletionProvider(this.db),
fields: async (query: SQLQuery) => {
if (!query?.dataset && !query?.table) {
return [];
}
return this.fetchFields(query);
},
validateQuery: (query) =>
Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }),
dsID: () => this.id,
dispose: (dsID?: string) => {},
toRawSql,
lookup: async (path?: string) => {
if (!path) {
const datasets = await this.fetchDatasets();
return datasets.map((d) => ({ name: d, completion: `${d}.${SCHEMA_NAME}.` }));
} else {
const parts = path.split('.').filter((s: string) => s);
if (parts.length > 2) {
return [];
}
if (parts.length === 1) {
const tables = await this.fetchTables(parts[0]);
return tables.map((t) => ({ name: t, completion: `${t}` }));
} else {
return [];
}
}
},
};
}
}

@ -1,34 +1,11 @@
import { DataSourcePlugin } from '@grafana/data';
import { SqlQueryEditor } from 'app/features/plugins/sql/components/QueryEditor';
import { SQLQuery } from 'app/features/plugins/sql/types';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
import { MssqlDatasource } from './datasource';
import { MssqlQueryCtrl } from './query_ctrl';
import { MssqlQuery } from './types';
import { MssqlOptions } from './types';
const defaultQuery = `SELECT
<time_column> as time,
<text_column> as text,
<tags_column> as tags
FROM
<table name>
WHERE
$__timeFilter(time_column)
ORDER BY
<time_column> ASC`;
class MssqlAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
declare annotation: any;
/** @ngInject */
constructor($scope: any) {
this.annotation = $scope.ctrl.annotation;
this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery;
}
}
export const plugin = new DataSourcePlugin<MssqlDatasource, MssqlQuery>(MssqlDatasource)
.setQueryCtrl(MssqlQueryCtrl)
.setConfigEditor(ConfigurationEditor)
.setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl);
export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery, MssqlOptions>(MssqlDatasource)
.setQueryEditor(SqlQueryEditor)
.setConfigEditor(ConfigurationEditor);

@ -1,90 +0,0 @@
<query-editor-row query-ctrl="ctrl" can-collapse="false">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sqlserver" textarea-label="Query Editor">
</code-editor>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword" for="format-select-{{ ctrl.target.refId }}">Format as</label>
<div class="gf-form-select-wrapper">
<select id="format-select-{{ ctrl.target.refId }}" class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
Show Help
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
Generated SQL
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
- return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
- any other columns returned will be the time point values.
Optional:
- return column named <i>metric</i> to represent the series name.
- If multiple value columns are returned the metric column is used as prefix.
- If no column named metric is found the column name of the value column is used as series name
Resultsets of time series queries need to be sorted by time.
Table:
- return any set of columns
Macros:
- $__time(column) -&gt; column AS time
- $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
- $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
- $__unixEpochNanoFilter(column) -&gt; column &gt;= 1494410783152415214 AND column &lt;= 1494497183142514872
- $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300.
by setting fillvalue grafana will fill in missing values according to the interval
fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
- $__timeGroupAlias(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
- $__unixEpochGroup(column,'5m') -&gt; FLOOR(column/300)*300
- $__unixEpochGroupAlias(column,'5m') -&gt; FLOOR(column/300)*300 AS [time]
Example of group by and order by with $__timeGroup:
SELECT
$__timeGroup(date_time_col, '1h') AS time,
sum(value) as value
FROM yourtable
GROUP BY $__timeGroup(date_time_col, '1h')
ORDER BY 1
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; '2017-04-21T05:01:17Z'
- $__timeTo() -&gt; '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
- $__unixEpochNanoFrom() -&gt; 1494410783152415214
- $__unixEpochNanoTo() -&gt; 1494497183142514872
</pre>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.executedQueryString}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
</query-editor-row>

@ -1,65 +0,0 @@
import { auto } from 'angular';
import { PanelEvents, QueryResultMeta } from '@grafana/data';
import { QueryCtrl } from 'app/plugins/sdk';
import { MssqlQuery } from './types';
const defaultQuery = `SELECT
$__timeEpoch(<time_column>),
<value column> as value,
<series name column> as metric
FROM
<table name>
WHERE
$__timeFilter(time_column)
ORDER BY
<time_column> ASC`;
export class MssqlQueryCtrl extends QueryCtrl<MssqlQuery> {
static templateUrl = 'partials/query.editor.html';
formats: any[];
lastQueryMeta?: QueryResultMeta;
lastQueryError?: string;
showHelp = false;
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService) {
super($scope, $injector);
this.target.format = this.target.format || 'time_series';
this.target.alias = '';
this.formats = [
{ text: 'Time series', value: 'time_series' },
{ text: 'Table', value: 'table' },
];
if (!this.target.rawSql) {
// special handling when in table panel
if (this.panelCtrl.panel.type === 'table') {
this.target.format = 'table';
this.target.rawSql = 'SELECT 1';
} else {
this.target.rawSql = defaultQuery;
}
}
this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope);
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
}
onDataReceived(dataList: any) {
this.lastQueryError = undefined;
this.lastQueryMeta = dataList[0]?.meta;
}
onDataError(err: any) {
if (err.data && err.data.results) {
const queryRes = err.data.results[this.target.refId];
if (queryRes) {
this.lastQueryError = queryRes.error;
}
}
}
}

@ -1,18 +1,10 @@
import { uniqBy } from 'lodash';
import { AnnotationEvent, DataFrame, MetricFindValue } from '@grafana/data';
import { BackendDataSourceResponse, toDataQueryResponse, FetchResponse } from '@grafana/runtime';
export default class ResponseParser {
transformMetricFindResponse(raw: FetchResponse<BackendDataSourceResponse>): MetricFindValue[] {
const frames = toDataQueryResponse(raw).data as DataFrame[];
if (!frames || !frames.length) {
return [];
}
const frame = frames[0];
import { DataFrame, MetricFindValue } from '@grafana/data';
import { ResponseParser } from 'app/features/plugins/sql/types';
export class MSSqlResponseParser implements ResponseParser {
transformMetricFindResponse(frame: DataFrame): MetricFindValue[] {
const values: MetricFindValue[] = [];
const textField = frame.fields.find((f) => f.name === '__text');
const valueField = frame.fields.find((f) => f.name === '__value');
@ -33,41 +25,4 @@ export default class ResponseParser {
return uniqBy(values, 'text');
}
async transformAnnotationResponse(options: any, data: BackendDataSourceResponse): Promise<AnnotationEvent[]> {
const frames = toDataQueryResponse({ data: data }).data as DataFrame[];
if (!frames || !frames.length) {
return [];
}
const frame = frames[0];
const timeField = frame.fields.find((f) => f.name === 'time');
if (!timeField) {
return Promise.reject({ message: 'Missing mandatory time column (with time column alias) in annotation query.' });
}
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(timeField.values.get(i)),
timeEnd,
text: textField && textField.values.get(i) ? textField.values.get(i) : '',
tags:
tagsField && tagsField.values.get(i)
? tagsField.values
.get(i)
.trim()
.split(/\s*,\s*/)
: [],
});
}
return list;
}
}

@ -0,0 +1,136 @@
import {
ColumnDefinition,
CompletionItemKind,
CompletionItemPriority,
LanguageCompletionProvider,
LinkedToken,
StatementPlacementProvider,
SuggestionKindProvider,
TableDefinition,
TokenType,
} from '@grafana/experimental';
import { AGGREGATE_FNS, OPERATORS } from 'app/features/plugins/sql/constants';
import { DB, SQLQuery } from 'app/features/plugins/sql/types';
import { SCHEMA_NAME } from './sqlUtil';
interface CompletionProviderGetterArgs {
getColumns: React.MutableRefObject<(t: SQLQuery) => Promise<ColumnDefinition[]>>;
getTables: React.MutableRefObject<(d?: string) => Promise<TableDefinition[]>>;
}
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider =
({ getColumns, getTables }) =>
() => ({
triggerCharacters: ['.', ' ', '$', ',', '(', "'"],
tables: {
resolve: async () => {
return await getTables.current();
},
parseName: (token: LinkedToken) => {
let processedToken = token;
let tablePath = processedToken.value;
while (processedToken.next && processedToken.next.type !== TokenType.Whitespace) {
tablePath += processedToken.next.value;
processedToken = processedToken.next;
}
const tableName = tablePath.split('.').pop();
return tableName || tablePath;
},
},
columns: {
resolve: async (t: string) => {
return await getColumns.current({ table: t, refId: 'A' });
},
},
supportedFunctions: () => AGGREGATE_FNS,
supportedOperators: () => OPERATORS,
customSuggestionKinds: customSuggestionKinds(getTables, getColumns),
customStatementPlacement,
});
export enum CustomStatementPlacement {
AfterDatabase = 'afterDatabase',
}
export enum CustomSuggestionKind {
TablesWithinDatabase = 'tablesWithinDatabase',
}
export const customStatementPlacement: StatementPlacementProvider = () => [
{
id: CustomStatementPlacement.AfterDatabase,
resolve: (currentToken, previousKeyword) => {
return Boolean(
currentToken?.is(TokenType.Delimiter, '.') ||
(currentToken?.is(TokenType.Whitespace) && currentToken?.previous?.is(TokenType.Delimiter, '.')) ||
(currentToken?.isNumber() && currentToken.value.endsWith('.'))
);
},
},
];
export const customSuggestionKinds: (
getTables: CompletionProviderGetterArgs['getTables'],
getFields: CompletionProviderGetterArgs['getColumns']
) => SuggestionKindProvider = (getTables) => () =>
[
{
id: CustomSuggestionKind.TablesWithinDatabase,
applyTo: [CustomStatementPlacement.AfterDatabase],
suggestionsResolver: async (ctx) => {
const tablePath = ctx.currentToken ? getDatabaseName(ctx.currentToken) : '';
const t = await getTables.current(tablePath);
return t.map((table) => ({
label: table.name,
insertText: table.completion ?? table.name,
command: { id: 'editor.action.triggerSuggest', title: '' },
kind: CompletionItemKind.Field,
sortText: CompletionItemPriority.High,
range: {
...ctx.range,
startColumn: ctx.range.endColumn,
endColumn: ctx.range.endColumn,
},
}));
},
},
];
export function getDatabaseName(token: LinkedToken) {
let processedToken = token;
let database = '';
while (processedToken?.previous && !processedToken.previous.isWhiteSpace()) {
processedToken = processedToken.previous;
database = processedToken.value + database;
}
if (database.includes(SCHEMA_NAME)) {
database = database.replace(SCHEMA_NAME, '');
}
database = database.trim();
return database;
}
export async function fetchColumns(db: DB, q: SQLQuery) {
const cols = await db.fields(q);
if (cols.length > 0) {
return cols.map((c) => {
return { name: c.value, type: c.value, description: c.value };
});
} else {
return [];
}
}
export async function fetchTables(db: DB, dataset?: string) {
const tables = await db.lookup(dataset);
return tables;
}

@ -0,0 +1,131 @@
import { isEmpty } from 'lodash';
import { RAQBFieldTypes, SQLExpression, SQLQuery } from 'app/features/plugins/sql/types';
import { haveColumns } from 'app/features/plugins/sql/utils/sql.utils';
export function getIcon(type: string): string | undefined {
switch (type) {
case 'datetimeoffset':
case 'date':
case 'datetime2':
case 'smalldatetime':
case 'datetime':
case 'time':
return 'clock-nine';
case 'bit':
return 'toggle-off';
case 'tinyint':
case 'smallint':
case 'int':
case 'bigint':
case 'decimal':
case 'numeric':
case 'real':
case 'float':
case 'money':
case 'smallmoney':
return 'calculator-alt';
case 'char':
case 'varchar':
case 'text':
case 'nchar':
case 'nvarchar':
case 'ntext':
case 'binary':
case 'varbinary':
case 'image':
return 'text';
default:
return undefined;
}
}
export function getRAQBType(type: string): RAQBFieldTypes {
switch (type) {
case 'datetimeoffset':
case 'datetime2':
case 'smalldatetime':
case 'datetime':
return 'datetime';
case 'time':
return 'time';
case 'date':
return 'date';
case 'bit':
return 'boolean';
case 'tinyint':
case 'smallint':
case 'int':
case 'bigint':
case 'decimal':
case 'numeric':
case 'real':
case 'float':
case 'money':
case 'smallmoney':
return 'number';
case 'char':
case 'varchar':
case 'text':
case 'nchar':
case 'nvarchar':
case 'ntext':
case 'binary':
case 'varbinary':
case 'image':
return 'text';
default:
return 'text';
}
}
export const SCHEMA_NAME = 'dbo';
export function toRawSql({ sql, dataset, table }: SQLQuery): string {
let rawQuery = '';
// Return early with empty string if there is no sql column
if (!sql || !haveColumns(sql.columns)) {
return rawQuery;
}
rawQuery += createSelectClause(sql.columns, sql.limit);
if (dataset && table) {
rawQuery += `FROM ${dataset}.${SCHEMA_NAME}.${table} `;
}
if (sql.whereString) {
rawQuery += `WHERE ${sql.whereString} `;
}
if (sql.groupBy?.[0]?.property.name) {
const groupBy = sql.groupBy.map((g) => g.property.name).filter((g) => !isEmpty(g));
rawQuery += `GROUP BY ${groupBy.join(', ')} `;
}
if (sql.orderBy?.property.name) {
rawQuery += `ORDER BY ${sql.orderBy.property.name} `;
}
if (sql.orderBy?.property.name && sql.orderByDirection) {
rawQuery += `${sql.orderByDirection} `;
}
return rawQuery;
}
function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>, limit?: number): string {
const columns = sqlColumns.map((c) => {
let rawColumn = '';
if (c.name) {
rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)})`;
} else {
rawColumn += `${c.parameters?.map((p) => `${p.name}`)}`;
}
return rawColumn;
});
return `SELECT ${isLimit(limit) ? 'TOP(' + limit + ')' : ''} ${columns.join(', ')} `;
}
const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0;

@ -1,21 +1,4 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
export interface MssqlQueryForInterpolation {
alias?: any;
format?: any;
rawSql?: any;
refId: any;
hide?: any;
}
export type ResultFormat = 'time_series' | 'table';
export interface MssqlQuery extends DataQuery {
alias?: string;
format?: ResultFormat;
rawSql?: any;
}
import { SQLOptions } from 'app/features/plugins/sql/types';
export enum MSSQLAuthenticationType {
sqlAuth = 'SQL Server Authentication',
@ -27,14 +10,9 @@ export enum MSSQLEncryptOptions {
false = 'false',
true = 'true',
}
export interface MssqlOptions extends DataSourceJsonData, SQLConnectionLimits {
authenticationType: MSSQLAuthenticationType;
encrypt: MSSQLEncryptOptions;
serverName: string;
sslRootCertFile: string;
tlsSkipVerify: boolean;
url: string;
database: string;
timeInterval: string;
user: string;
export interface MssqlOptions extends SQLOptions {
authenticationType?: MSSQLAuthenticationType;
encrypt?: MSSQLEncryptOptions;
sslRootCertFile?: string;
serverName?: string;
}

@ -1,5 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
import { SQLConnectionLimits } from 'app/features/plugins/sql/types';
export interface MysqlQueryForInterpolation {
alias?: any;
format?: any;

@ -1,5 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
import { SQLConnectionLimits } from 'app/features/plugins/sql/types';
export enum PostgresTLSModes {
disable = 'disable',

Loading…
Cancel
Save