MSSQL: Migrate query editor to React

This commit is contained in:
Zoltán Bedi
2022-07-05 20:44:22 +02:00
parent fee2eb3716
commit 675898caea
12 changed files with 513 additions and 533 deletions
-21
View File
@@ -7735,27 +7735,6 @@ exports[`better eslint`] = {
[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/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/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/mysql/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 +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
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,24 @@
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';
export class MSSqlQueryModel implements SqlQueryModel {
target: SQLQuery;
templateSrv?: TemplateSrv;
scopedVars?: ScopedVars;
constructor(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.target = applyQueryDefaults(target);
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
}
interpolate() {
return this.templateSrv?.replace(this.target.rawSql, this.scopedVars, this.target.format) || '';
}
quoteLiteral(value: string) {
return "'" + value.replace(/'/g, "''") + "'";
}
}
@@ -1,85 +1,47 @@
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 { SQLOptions, 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';
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<SQLOptions>;
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 +80,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 +102,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 +117,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 +143,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 +161,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 +182,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 +209,10 @@ describe('MSSQLDatasource', () => {
},
},
};
const time = {
const time: TimeRange = {
from: dateTime(1521545610656),
to: dateTime(1521546251185),
raw: { from: '1521545610656', to: '1521546251185' },
};
beforeEach(() => {
@@ -268,10 +231,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 +273,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 +286,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 +312,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();
});
});
});
+79 -179
View File
@@ -1,191 +1,91 @@
import { map as _map } from 'lodash';
import { lastValueFrom, of } from 'rxjs';
import { catchError, map, mapTo } from 'rxjs/operators';
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, SQLOptions } from 'app/features/plugins/sql/types';
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 { getSchema, showDatabases, showTables } from './MSSqlMetaQuery';
import { MSSqlQueryModel } from './MSSqlQueryModel';
import { MSSqlResponseParser } from './response_parser';
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
import { getIcon, getRAQBType, toRawSql } from './sqlUtil';
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';
export class MssqlDatasource extends SqlDatasource {
completionProvider: LanguageCompletionProvider | undefined = undefined;
constructor(instanceSettings: DataSourceInstanceSettings<SQLOptions>, 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> {
async fetchDatasets(): Promise<string[]> {
const datasets = await this.metricFindQuery(showDatabases(), {});
return datasets.map((t) => t.text);
}
async fetchTables(dataset?: string): Promise<string[]> {
const tables = await this.metricFindQuery(showTables(dataset), {});
return tables.map((t) => t.text);
}
async fetchFields(query: SQLQuery): Promise<SQLSelectableValue[]> {
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.table));
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;
}
getSqlCompletionProvider(db: DB): LanguageCompletionProvider {
if (this.completionProvider !== undefined) {
return this.completionProvider;
}
const args = {
getColumns: { current: (query: SQLQuery) => fetchColumns(db, query) },
getTables: { current: (dataset?: string) => fetchTables(db, dataset) },
};
this.completionProvider = getSqlCompletionProvider(args);
return this.completionProvider;
}
getDB(): DB {
return {
refId: target.refId,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(target.rawSql, scopedVars, this.interpolateVariable),
format: target.format,
init: () => Promise.resolve(true),
datasets: () => this.fetchDatasets(),
tables: (dataset?: string) => this.fetchTables(dataset),
getSqlCompletionProvider: () => this.getSqlCompletionProvider(this.db),
fields: (query: SQLQuery) => 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}.` }));
} 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 [];
}
}
},
};
}
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)
)
)
);
}
filterQuery(query: MssqlQuery): boolean {
return !query.hide;
}
metricFindQuery(query: string, optionalOptions: any): Promise<MetricFindValue[]> {
let refId = 'tempvar';
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
refId = optionalOptions.variable.name;
}
const range = optionalOptions?.range as TimeRange;
const interpolatedQuery = {
refId: refId,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable),
format: 'table',
};
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));
})
)
);
}
targetContainsTemplate(query: MssqlQuery): boolean {
const rawSql = query.rawSql.replace('$__', '');
return this.templateSrv.containsTemplate(rawSql);
}
}
+5 -29
View File
@@ -1,34 +1,10 @@
import { DataSourcePlugin } from '@grafana/data';
import { SqlQueryEditor } from 'app/features/plugins/sql/components/QueryEditor';
import { SQLQuery } from 'app/features/plugins/sql/types';
import { MssqlConfigCtrl } from './config_ctrl';
import { MssqlDatasource } from './datasource';
import { MssqlQueryCtrl } from './query_ctrl';
import { MssqlQuery } 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)
.setConfigCtrl(MssqlConfigCtrl)
.setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl);
export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery>(MssqlDatasource)
.setQueryEditor(SqlQueryEditor)
.setConfigCtrl(MssqlConfigCtrl);
@@ -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,212 @@
// import { useCallback } from 'react';
import {
ColumnDefinition,
CompletionItemKind,
CompletionItemPriority,
LanguageCompletionProvider,
LinkedToken,
StatementPlacementProvider,
// StatementPosition,
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';
interface CompletionProviderGetterArgs {
getColumns: React.MutableRefObject<(t: SQLQuery) => Promise<ColumnDefinition[]>>;
getTables: React.MutableRefObject<(d?: string) => Promise<TableDefinition[]>>;
// getTableSchema: React.MutableRefObject<(p: string, d: string, t: string) => Promise<TableSchema | null>>;
}
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) {
tablePath += processedToken.next.value;
processedToken = processedToken.next;
}
return tablePath;
},
},
columns: {
resolve: async (t: string) => {
// TODO - seems like a limitation in experimental since we may need database and table to get columns
// use . as delimiter?
// const cols = await getColumns({ table: t } as SQLQuery);
return await getColumns.current({ table: t, refId: 'A' });
},
},
supportedFunctions: () => AGGREGATE_FNS,
supportedOperators: () => OPERATORS,
customSuggestionKinds: customSuggestionKinds(getTables, getColumns),
customStatementPlacement,
});
export enum CustomStatementPlacement {
AfterDataset = 'afterDataset',
}
export enum CustomSuggestionKind {
TablesWithinDataset = 'tablesWithinDataset',
Partition = 'partition',
}
export const customStatementPlacement: StatementPlacementProvider = () => [
{
id: CustomStatementPlacement.AfterDataset,
resolve: (currentToken, previousKeyword) => {
return Boolean(
currentToken?.is(TokenType.Delimiter, '.') ||
(currentToken?.is(TokenType.Whitespace) && currentToken?.previous?.is(TokenType.Delimiter, '.')) ||
// (currentToken?.value === '`' && currentToken?.previous?.is(TokenType.Delimiter, '.')) ||
(currentToken?.isNumber() && currentToken.value.endsWith('.')) // number with dot at the end like "projectname-21342."
// (currentToken?.value === '`' && isTypingTableIn(currentToken))
);
},
},
// TODO - remove - frin big query
// Overriding default behaviour of AfterFrom resolver
// {
// id: StatementPosition.AfterFrom,
// overrideDefault: true,
// resolve: (currentToken) => {
// const untilFrom = currentToken?.getPreviousUntil(TokenType.Keyword, [], 'from');
// if (!untilFrom) {
// return false;
// }
// let q = '';
// for (let i = untilFrom?.length - 1; i >= 0; i--) {
// q += untilFrom[i].value;
// }
// return q.startsWith('`') && q.endsWith('`');
// },
// },
];
export const customSuggestionKinds: (
getTables: CompletionProviderGetterArgs['getTables'],
getFields: CompletionProviderGetterArgs['getColumns']
// getTableSchema: CompletionProviderGetterArgs['getTableSchema']
) => SuggestionKindProvider = (getTables) => () =>
[
{
id: CustomSuggestionKind.TablesWithinDataset,
applyTo: [CustomStatementPlacement.AfterDataset],
suggestionsResolver: async (ctx) => {
const tablePath = ctx.currentToken ? getTablePath(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,
},
}));
},
},
// {
// id: CustomSuggestionKind.TablesWithinDataset,
// applyTo: [StatementPosition.AfterFrom],
// suggestionsResolver: async (ctx) => {
// console.log('after from');
// const tablePath = ctx.currentToken ? getTablePath(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 getTablePath(token: LinkedToken) {
let processedToken = token;
let tablePath = '';
while (processedToken?.previous && !processedToken.previous.isWhiteSpace()) {
processedToken = processedToken.previous;
tablePath = processedToken.value + tablePath;
}
tablePath = tablePath.trim();
// TODO - remove - frin big query
// if (tablePath.startsWith('`')) {
// tablePath = tablePath.slice(1);
// }
// if (tablePath.endsWith('`')) {
// tablePath = tablePath.slice(0, -1);
// }
return tablePath;
}
// TODO - not sure if we need this
// function isTypingTableIn(token: LinkedToken | null, l?: boolean) {
// if (!token) {
// return false;
// }
// const tokens = token.getPreviousUntil(TokenType.Keyword, [], 'from');
// if (!tokens) {
// return false;
// }
// let path = '';
// for (let i = tokens.length - 1; i >= 0; i--) {
// path += tokens[i].value;
// }
// // TODO - remove - frin big query
// // if (path.startsWith('`')) {
// // path = path.slice(1);
// // }
// return path.split('.').length === 2;
// }
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,129 @@
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 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}.dbo.${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 +0,0 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
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;
}
export interface MssqlOptions extends DataSourceJsonData {
timeInterval: string;
}