mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
7b40322bbe
commit
35d98104ad
@ -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 SQLConnectionLimits {
|
||||
maxOpenConns: number;
|
||||
maxIdleConns: number;
|
||||
connMaxLifetime: number;
|
||||
}
|
||||
|
||||
export interface SQLOptions extends DataSourceJsonData {
|
||||
timeInterval: string;
|
||||
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 {
|
||||
|
15
public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts
Normal file
15
public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts
Normal file
@ -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}';`;
|
||||
}
|
25
public/app/plugins/datasource/mssql/MSSqlQueryModel.ts
Normal file
25
public/app/plugins/datasource/mssql/MSSqlQueryModel.ts
Normal file
@ -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 { 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 { 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, SCHEMA_NAME, toRawSql } from './sqlUtil';
|
||||
import { MssqlOptions } from './types';
|
||||
|
||||
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<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> {
|
||||
async fetchDatasets(): Promise<string[]> {
|
||||
const datasets = await this.runSql<{ name: string[] }>(showDatabases(), { refId: 'datasets' });
|
||||
return datasets.fields.name.values.toArray().flat();
|
||||
}
|
||||
|
||||
async fetchTables(dataset?: string): Promise<string[]> {
|
||||
const tables = await this.runSql<{ name: string[] }>(showTables(dataset), { refId: 'tables' });
|
||||
return tables.fields.name.values.toArray().flat();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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: 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 [];
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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) -> column AS time
|
||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||
- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
|
||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||
- $__unixEpochNanoFilter(column) -> column >= 1494410783152415214 AND column <= 1494497183142514872
|
||||
- $__timeGroup(column, '5m'[, fillvalue]) -> 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]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
|
||||
- $__unixEpochGroup(column,'5m') -> FLOOR(column/300)*300
|
||||
- $__unixEpochGroupAlias(column,'5m') -> 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() -> '2017-04-21T05:01:17Z'
|
||||
- $__timeTo() -> '2017-04-21T05:01:17Z'
|
||||
- $__unixEpochFrom() -> 1492750877
|
||||
- $__unixEpochTo() -> 1492750877
|
||||
- $__unixEpochNanoFrom() -> 1494410783152415214
|
||||
- $__unixEpochNanoTo() -> 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;
|
||||
}
|
||||
}
|
||||
|
136
public/app/plugins/datasource/mssql/sqlCompletionProvider.ts
Normal file
136
public/app/plugins/datasource/mssql/sqlCompletionProvider.ts
Normal file
@ -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;
|
||||
}
|
131
public/app/plugins/datasource/mssql/sqlUtil.ts
Normal file
131
public/app/plugins/datasource/mssql/sqlUtil.ts
Normal file
@ -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…
Reference in New Issue
Block a user