├── .gitignore
├── .jscs.json
├── Gruntfile.js
├── LICENSE
├── README.md
├── dist
├── README.md
├── css
│ └── query-editor.css
├── datasource.js
├── datasource.js.map
├── img
│ ├── logo_large.png
│ └── logo_small.png
├── module.js
├── module.js.map
├── partials
│ ├── annotations.editor.html
│ ├── config.html
│ ├── query.editor.html
│ └── query.options.html
├── plugin.json
├── query_ctrl.js
├── query_ctrl.js.map
└── serverside
│ ├── drilldriver.js
│ ├── mssqldriver.js
│ └── sqlproxyserver.js
├── overview.png
├── package.json
├── query_editor.png
├── spec
├── datasource_spec.js
├── sqlproxyserver_spec.js
└── test-main.js
└── src
├── css
└── query-editor.css
├── datasource.js
├── img
├── logo_large.png
└── logo_small.png
├── module.js
├── partials
├── annotations.editor.html
├── config.html
├── query.editor.html
└── query.options.html
├── plugin.json
├── query_ctrl.js
└── serverside
├── drilldriver.js
├── mssqldriver.js
├── sqlproxy.service
└── sqlproxyserver.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage/
4 | .aws-config.json
5 | awsconfig
6 | /emails/dist
7 | /public_gen
8 | /tmp
9 | vendor/phantomjs/phantomjs
10 |
11 | docs/AWS_S3_BUCKET
12 | docs/GIT_BRANCH
13 | docs/VERSION
14 | docs/GITCOMMIT
15 | docs/changed-files
16 | docs/changed-files
17 |
18 | # locally required config files
19 | public/css/*.min.css
20 |
21 | # Editor junk
22 | *.sublime-workspace
23 | *.swp
24 | .idea/
25 | *.iml
26 |
27 | /data/*
28 | /bin/*
29 |
30 | conf/custom.ini
31 | fig.yml
32 | profile.cov
33 | grafana
34 | .notouch
35 |
36 | # Test artifacts
37 | /dist/test/
38 | deploy.bat
39 | .vscode/
40 |
--------------------------------------------------------------------------------
/.jscs.json:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true,
3 | "disallowImplicitTypeConversion": ["string"],
4 | "disallowKeywords": ["with"],
5 | "disallowMultipleLineBreaks": true,
6 | "disallowMixedSpacesAndTabs": true,
7 | "disallowTrailingWhitespace": true,
8 | "requireSpacesInFunctionExpression": {
9 | "beforeOpeningCurlyBrace": true
10 | },
11 | "disallowSpacesInsideArrayBrackets": true,
12 | "disallowSpacesInsideParentheses": true,
13 | "validateIndentation": 2
14 | }
15 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | require('load-grunt-tasks')(grunt);
4 |
5 | grunt.loadNpmTasks('grunt-execute');
6 | grunt.loadNpmTasks('grunt-contrib-clean');
7 |
8 | grunt.initConfig({
9 |
10 | clean: ["dist"],
11 |
12 | copy: {
13 | src_to_dist: {
14 | cwd: 'src',
15 | expand: true,
16 | src: ['**/*', '!**/*.js', '!**/*.scss','./bin/*','./serverside/*'],
17 | dest: 'dist'
18 | },
19 | pluginDef: {
20 | expand: true,
21 | src: ['README.md'],
22 | dest: 'dist'
23 | }
24 | },
25 |
26 | watch: {
27 | rebuild_all: {
28 | files: ['src/**/*'],
29 | tasks: ['default'],
30 | options: {spawn: false}
31 | }
32 | },
33 |
34 | babel: {
35 | options: {
36 | sourceMap: true,
37 | presets: ['es2015']
38 | },
39 | dist: {
40 | options: {
41 | plugins: ['transform-es2015-modules-systemjs', 'transform-es2015-for-of']
42 | },
43 | files: [{
44 | cwd: 'src',
45 | expand: true,
46 | src: ['**/*.js','!**/server.js','!serverside/*'],
47 | dest: 'dist',
48 | ext:'.js'
49 | }]
50 | },
51 | distTestNoSystemJs: {
52 | files: [{
53 | cwd: 'src',
54 | expand: true,
55 | src: ['**/*.js'],
56 | dest: 'dist/test',
57 | ext:'.js'
58 | }]
59 | },
60 | distTestsSpecsNoSystemJs: {
61 | files: [{
62 | expand: true,
63 | cwd: 'spec',
64 | src: ['**/*.js'],
65 | dest: 'dist/test/spec',
66 | ext:'.js'
67 | }]
68 | }
69 | },
70 |
71 | mochaTest: {
72 | test: {
73 | options: {
74 | reporter: 'spec'
75 | },
76 | src: ['dist/test/spec/test-main.js', 'dist/test/spec/*_spec.js']
77 | }
78 | }
79 | });
80 |
81 | grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel', 'mochaTest']);
82 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Gustavo Brian
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/gbrian/repofunding) [](https://www.paypal.me/repofunding/5)
2 | # grafana-simple-sql-datasource
3 |
4 | Allows querying SQL based datasources like SQL Server.
5 |
6 | 
7 |
8 |
9 | ## Usage
10 | Currently the plugin requires a proxy server running to communicate with the database.
11 |
12 | **Install sqlproxyserver**
13 |
14 | * Run `npm install` at the `dist/serverside` folder to install all dependencies
15 | * Run npm install on the plugin directory
16 | * Run server side code `dist/serverside/sqlproxyserver.js`
17 | * Test on your browser `http://myserver:port/con=mssql://user:name@server/database` you must get a `{"status":"sucess"}` response
18 |
19 | **Add new datasource**
20 | Add a new datasource to Grafana and set the url to:
21 |
22 | ````
23 | http://myserver:port/con=mssql://user:name@server/database
24 | ````
25 |
26 | Where:
27 |
28 | * **myserver:port** : Is the server where `sqlproxyserver` is running
29 | * **con**: Specifies the sql connection string
30 |
31 | ## SQL Databases
32 | Currently supported SQL databases
33 |
34 | ### SQL Server
35 | SQL Server connection is managed by the mssqp package https://www.npmjs.com/package/mssql
36 |
37 | ## Features
38 | Following features has been implemented
39 |
40 | 
41 |
42 | ### Metrics
43 | It is possible to define two different types: `timeseries` and `table`
44 |
45 | ### Annotation
46 | Annotation querires must return the following fields:
47 |
48 | * **title**: Annotation header
49 | * **text**: Annotation description
50 | * **tags**: Annotation tags
51 | * **time**: Annotation time
52 |
53 | ## Notes
54 | ### Time
55 | UTC and Localtime. Currently you must specify if time returned by the query is UTC or local.
56 | The plugin will convert localtime to UTC in order to be correctly renderer.
57 | ### Template
58 | You can use `$from` and `$to` to refer to selected time period in your queries like:
59 |
60 | ````
61 | select 'Metric Name' as metric, -- Use a literal or group by a column for the labels
62 | count(*) as hits, -- Just counting occurrences
63 | ts as [timestamp]
64 | from (
65 | Select dbo.scale_interval(dateColumn, '$Interval') as ts -- scale datetime to $Interval (e.g. 10m)
66 | from myTable
67 | where dateColumn >= '$from' and dateColumn < '$to'
68 | ) T
69 | group by ts
70 | order by ts asc
71 | ````
72 |
73 | ### MISC
74 | #### scale_interval
75 | Simple TSQL to group series by an interval
76 |
77 | ````
78 | ALTER FUNCTION scale_interval
79 | (
80 | -- Add the parameters for the function here
81 | @dt as datetime, @interval as varchar(100)
82 | )
83 | RETURNS DateTime
84 | AS
85 | BEGIN
86 | DECLARE @amount int = 10
87 |
88 | IF CHARINDEX('m', @interval) <> 0
89 | BEGIN
90 | SET @amount = CAST(REPLACE(@interval, 'm', '') as int)
91 | return dateadd(minute, datediff(mi, 0, @dt) / @amount * @amount, 0)
92 | END
93 | IF CHARINDEX('h', @interval) <> 0
94 | BEGIN
95 | SET @amount = CAST(REPLACE(@interval, 'h', '') as int)
96 | return dateadd(hour, datediff(hour, 0, @dt) / @amount * @amount, 0)
97 | END
98 | IF CHARINDEX('d', @interval) <> 0
99 | BEGIN
100 | SET @amount = CAST(REPLACE(@interval, 'd', '') as int)
101 | return dateadd(day, datediff(day, 0, @dt) / @amount * @amount, 0)
102 | END
103 | RETURN NULL
104 | END
105 | GO
106 | ````
107 |
108 |
109 | ## Thanks to
110 | Grafana team and [@bergquist](https://github.com/bergquist)
111 |
112 | ## *Powered by @repofunding*
113 |
--------------------------------------------------------------------------------
/dist/README.md:
--------------------------------------------------------------------------------
1 | # grafana-simple-sql-datasource
2 |
3 | Allows querying SQL based datasources like SQL Server.
4 |
5 | 
6 |
7 |
8 | ## Usage
9 | Currently the plugin requires a proxy server running to communicate with the database.
10 |
11 | **Install sqlproxyserver**
12 |
13 | * Run `npm install` at the `dist/serverside` folder to install all dependencies
14 | * Run npm install on the plugin directory
15 | * Run server side code `dist/serverside/sqlproxyserver.js`
16 | * Test on your browser `http://myserver:port/con=mssql://user:name@server/database` you must get a `{"status":"sucess"}` response
17 |
18 | **Add new datasource**
19 | Add a new datasource to Grafana and set the url to:
20 |
21 | ````
22 | http://myserver:port/con=mssql://user:name@server/database
23 | ````
24 |
25 | Where:
26 |
27 | * **myserver:port** : Is the server where `sqlproxyserver` is running
28 | * **con**: Specifies the sql connection string
29 |
30 | ## SQL Databases
31 | Currently supported SQL databases
32 |
33 | ### SQL Server
34 | SQL Server connection is managed by the mssqp package https://www.npmjs.com/package/mssql
35 |
36 | ## Features
37 | Following features has been implemented
38 |
39 | 
40 |
41 | ### Metrics
42 | It is possible to define two different types: `timeseries` and `table`
43 |
44 | ### Annotation
45 | Annotation querires must return the following fields:
46 |
47 | * **title**: Annotation header
48 | * **text**: Annotation description
49 | * **tags**: Annotation tags
50 | * **time**: Annotation time
51 |
52 | ## Notes
53 | ### Time
54 | UTC and Localtime. Currently you must specify if time returned by the query is UTC or local.
55 | The plugin will convert localtime to UTC in order to be correctly renderer.
56 | ### Template
57 | You can use `$from` and `$to` to refer to selected time period in your queries like:
58 |
59 | ````
60 | SELECT field FROM table WHERE datestart >= '$from' AND dateStart <= '$to'
61 | ````
62 |
63 | ## Thanks to
64 | Grafana team and [@bergquist](https://github.com/bergquist)
65 |
66 |
--------------------------------------------------------------------------------
/dist/css/query-editor.css:
--------------------------------------------------------------------------------
1 | .grafana-simple-sql-datasource .hidden {
2 | display: none;
3 | }
4 |
5 | .ace-sql-editor{
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 | }
--------------------------------------------------------------------------------
/dist/datasource.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | System.register(["lodash"], function (_export, _context) {
4 | "use strict";
5 |
6 | var _, _createClass, GenericDatasource;
7 |
8 | function _classCallCheck(instance, Constructor) {
9 | if (!(instance instanceof Constructor)) {
10 | throw new TypeError("Cannot call a class as a function");
11 | }
12 | }
13 |
14 | return {
15 | setters: [function (_lodash) {
16 | _ = _lodash.default;
17 | }],
18 | execute: function () {
19 | _createClass = function () {
20 | function defineProperties(target, props) {
21 | for (var i = 0; i < props.length; i++) {
22 | var descriptor = props[i];
23 | descriptor.enumerable = descriptor.enumerable || false;
24 | descriptor.configurable = true;
25 | if ("value" in descriptor) descriptor.writable = true;
26 | Object.defineProperty(target, descriptor.key, descriptor);
27 | }
28 | }
29 |
30 | return function (Constructor, protoProps, staticProps) {
31 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
32 | if (staticProps) defineProperties(Constructor, staticProps);
33 | return Constructor;
34 | };
35 | }();
36 |
37 | _export("GenericDatasource", GenericDatasource = function () {
38 | function GenericDatasource(instanceSettings, $q, backendSrv, templateSrv) {
39 | _classCallCheck(this, GenericDatasource);
40 |
41 | this.type = instanceSettings.type;
42 | this.url = instanceSettings.url || "";
43 | var m = /con\=(.*)/.exec(this.url.split("?")[1]);
44 | this.connection = m ? m[1] : null;
45 | this.name = instanceSettings.name;
46 | this.q = $q;
47 | this.backendSrv = backendSrv;
48 | this.templateSrv = templateSrv;
49 | }
50 |
51 | _createClass(GenericDatasource, [{
52 | key: "buildRequest",
53 | value: function buildRequest(rqtype, data) {
54 | return {
55 | type: rqtype,
56 | body: data,
57 | url: this.connection
58 | };
59 | }
60 | }, {
61 | key: "query",
62 | value: function query(options) {
63 | var query = this.buildQueryParameters(options);
64 |
65 | if (query.targets.length <= 0) {
66 | return this.q.when({ data: [] });
67 | }
68 |
69 | return this.backendSrv.datasourceRequest({
70 | url: this.url,
71 | data: this.buildRequest("query", query),
72 | method: 'POST',
73 | headers: { 'Content-Type': 'application/json' }
74 | });
75 | }
76 | }, {
77 | key: "testDatasource",
78 | value: function testDatasource() {
79 | return this.backendSrv.datasourceRequest({
80 | url: this.url,
81 | method: 'POST',
82 | data: this.buildRequest("test", null)
83 | }).then(function (result) {
84 | return { status: "success", message: "Data source is working", title: "Success" };
85 | }).catch(function (result) {
86 | return { status: "error", message: result, title: "Error" };
87 | });
88 | }
89 | }, {
90 | key: "annotationQuery",
91 | value: function annotationQuery(options) {
92 | var annotationQuery = _.assignIn({}, options);
93 | annotationQuery.annotation.query = this.templateSrv.replace(options.annotation.query, {}, 'glob');
94 |
95 | return this.backendSrv.datasourceRequest({
96 | url: this.url,
97 | method: 'POST',
98 | data: this.buildRequest("annotations", annotationQuery)
99 | }).then(function (result) {
100 | return result.data;
101 | });
102 | }
103 | }, {
104 | key: "metricFindQuery",
105 | value: function metricFindQuery(options) {
106 | var opsAsString = typeof options === "string";
107 | if (options && options.type == 'sql')
108 | // TODO: Parser?
109 | return this.q.when([]);
110 | var target = opsAsString ? options : options.target;
111 | var interpolated = {
112 | target: this.templateSrv.replace(target, null, 'regex')
113 | };
114 |
115 | return this.backendSrv.datasourceRequest({
116 | url: this.url,
117 | data: this.buildRequest("search", interpolated),
118 | method: 'POST',
119 | headers: { 'Content-Type': 'application/json' }
120 | }).then(this.mapToTextValue);
121 | }
122 | }, {
123 | key: "mapToTextValue",
124 | value: function mapToTextValue(result) {
125 | return _.map(result.data, function (d, i) {
126 | if (d && d.text && d.value) {
127 | return { text: d.text, value: d.value };
128 | } else if (_.isObject(d)) {
129 | return { text: d, value: i };
130 | }
131 | return { text: d, value: d };
132 | });
133 | }
134 | }, {
135 | key: "buildQueryParameters",
136 | value: function buildQueryParameters(options) {
137 | var _this = this;
138 |
139 | var clonedOptions = _.cloneDeep(options);
140 | var targets = _.filter(clonedOptions.targets, function (target) {
141 | return target.target !== 'select metric' && !target.hide;
142 | });
143 |
144 | targets = _.map(targets, function (target) {
145 | return _.assignIn(target, { target: _this.templateSrv.replace(target.target, options.scopedVars, "distributed") });
146 | });
147 |
148 | clonedOptions.targets = targets;
149 |
150 | return clonedOptions;
151 | }
152 | }]);
153 |
154 | return GenericDatasource;
155 | }());
156 |
157 | _export("GenericDatasource", GenericDatasource);
158 | }
159 | };
160 | });
161 | //# sourceMappingURL=datasource.js.map
162 |
--------------------------------------------------------------------------------
/dist/datasource.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../src/datasource.js"],"names":["_","GenericDatasource","instanceSettings","$q","backendSrv","templateSrv","type","url","m","exec","split","connection","name","q","rqtype","data","body","options","query","buildQueryParameters","targets","length","when","datasourceRequest","buildRequest","method","headers","then","status","message","title","catch","result","annotationQuery","assignIn","annotation","replace","opsAsString","target","interpolated","mapToTextValue","map","d","i","text","value","isObject","clonedOptions","cloneDeep","filter","hide","scopedVars"],"mappings":";;;;;;;;;;;;;;;AAAOA,O;;;;;;;;;;;;;;;;;;;;;mCAEMC,iB;AAEX,mCAAYC,gBAAZ,EAA8BC,EAA9B,EAAkCC,UAAlC,EAA8CC,WAA9C,EAA2D;AAAA;;AACzD,eAAKC,IAAL,GAAYJ,iBAAiBI,IAA7B;AACA,eAAKC,GAAL,GAAWL,iBAAiBK,GAAjB,IAAwB,EAAnC;AACA,cAAIC,IAAI,YAAYC,IAAZ,CAAiB,KAAKF,GAAL,CAASG,KAAT,CAAe,GAAf,EAAoB,CAApB,CAAjB,CAAR;AACA,eAAKC,UAAL,GAAkBH,IAAIA,EAAE,CAAF,CAAJ,GAAU,IAA5B;AACA,eAAKI,IAAL,GAAYV,iBAAiBU,IAA7B;AACA,eAAKC,CAAL,GAASV,EAAT;AACA,eAAKC,UAAL,GAAkBA,UAAlB;AACA,eAAKC,WAAL,GAAmBA,WAAnB;AACD;;;;uCAEYS,M,EAAQC,I,EAAK;AACxB,mBAAO;AACLT,oBAAMQ,MADD;AAELE,oBAAMD,IAFD;AAGLR,mBAAK,KAAKI;AAHL,aAAP;AAKD;;;gCAEKM,O,EAAS;AACb,gBAAIC,QAAQ,KAAKC,oBAAL,CAA0BF,OAA1B,CAAZ;;AAEA,gBAAIC,MAAME,OAAN,CAAcC,MAAd,IAAwB,CAA5B,EAA+B;AAC7B,qBAAO,KAAKR,CAAL,CAAOS,IAAP,CAAY,EAACP,MAAM,EAAP,EAAZ,CAAP;AACD;;AAED,mBAAO,KAAKX,UAAL,CAAgBmB,iBAAhB,CAAkC;AACvChB,mBAAK,KAAKA,GAD6B;AAEvCQ,oBAAM,KAAKS,YAAL,CAAkB,OAAlB,EAA2BN,KAA3B,CAFiC;AAGvCO,sBAAQ,MAH+B;AAIvCC,uBAAS,EAAE,gBAAgB,kBAAlB;AAJ8B,aAAlC,CAAP;AAMD;;;2CAEgB;AACf,mBAAO,KAAKtB,UAAL,CAAgBmB,iBAAhB,CAAkC;AACvChB,mBAAK,KAAKA,GAD6B;AAEvCkB,sBAAQ,MAF+B;AAGvCV,oBAAM,KAAKS,YAAL,CAAkB,MAAlB,EAA0B,IAA1B;AAHiC,aAAlC,EAIJG,IAJI,CAIC,kBAAU;AAChB,qBAAO,EAAEC,QAAQ,SAAV,EAAqBC,SAAS,wBAA9B,EAAwDC,OAAO,SAA/D,EAAP;AACD,aANM,EAMJC,KANI,CAME,kBAAU;AACjB,qBAAO,EAAEH,QAAQ,OAAV,EAAmBC,SAASG,MAA5B,EAAoCF,OAAO,OAA3C,EAAP;AACD,aARM,CAAP;AASD;;;0CAEeb,O,EAAS;AACvB,gBAAIgB,kBAAkBjC,EAAEkC,QAAF,CAAW,EAAX,EAAejB,OAAf,CAAtB;AACAgB,4BAAgBE,UAAhB,CAA2BjB,KAA3B,GAAmC,KAAKb,WAAL,CAAiB+B,OAAjB,CAAyBnB,QAAQkB,UAAR,CAAmBjB,KAA5C,EAAmD,EAAnD,EAAuD,MAAvD,CAAnC;;AAEA,mBAAO,KAAKd,UAAL,CAAgBmB,iBAAhB,CAAkC;AACvChB,mBAAK,KAAKA,GAD6B;AAEvCkB,sBAAQ,MAF+B;AAGvCV,oBAAM,KAAKS,YAAL,CAAkB,aAAlB,EAAiCS,eAAjC;AAHiC,aAAlC,EAIJN,IAJI,CAIC,kBAAU;AAChB,qBAAOK,OAAOjB,IAAd;AACD,aANM,CAAP;AAOD;;;0CAEeE,O,EAAS;AACvB,gBAAIoB,cAAc,OAAQpB,OAAR,KAAqB,QAAvC;AACA,gBAAGA,WAAWA,QAAQX,IAAR,IAAgB,KAA9B;AACE;AACA,qBAAO,KAAKO,CAAL,CAAOS,IAAP,CAAY,EAAZ,CAAP;AACF,gBAAIgB,SAASD,cAAcpB,OAAd,GAAwBA,QAAQqB,MAA7C;AACA,gBAAIC,eAAe;AACfD,sBAAQ,KAAKjC,WAAL,CAAiB+B,OAAjB,CAAyBE,MAAzB,EAAiC,IAAjC,EAAuC,OAAvC;AADO,aAAnB;;AAIA,mBAAO,KAAKlC,UAAL,CAAgBmB,iBAAhB,CAAkC;AACvChB,mBAAK,KAAKA,GAD6B;AAEvCQ,oBAAM,KAAKS,YAAL,CAAkB,QAAlB,EAA4Be,YAA5B,CAFiC;AAGvCd,sBAAQ,MAH+B;AAIvCC,uBAAS,EAAE,gBAAgB,kBAAlB;AAJ8B,aAAlC,EAKJC,IALI,CAKC,KAAKa,cALN,CAAP;AAMD;;;yCAEcR,M,EAAQ;AACrB,mBAAOhC,EAAEyC,GAAF,CAAMT,OAAOjB,IAAb,EAAmB,UAAC2B,CAAD,EAAIC,CAAJ,EAAU;AAClC,kBAAID,KAAKA,EAAEE,IAAP,IAAeF,EAAEG,KAArB,EAA4B;AAC1B,uBAAO,EAAED,MAAMF,EAAEE,IAAV,EAAgBC,OAAOH,EAAEG,KAAzB,EAAP;AACD,eAFD,MAEO,IAAI7C,EAAE8C,QAAF,CAAWJ,CAAX,CAAJ,EAAmB;AACxB,uBAAO,EAAEE,MAAMF,CAAR,EAAWG,OAAOF,CAAlB,EAAP;AACD;AACD,qBAAO,EAAEC,MAAMF,CAAR,EAAWG,OAAOH,CAAlB,EAAP;AACD,aAPM,CAAP;AAQD;;;+CAEoBzB,O,EAAS;AAAA;;AAC5B,gBAAI8B,gBAAgB/C,EAAEgD,SAAF,CAAY/B,OAAZ,CAApB;AACA,gBAAIG,UAAUpB,EAAEiD,MAAF,CAASF,cAAc3B,OAAvB,EAAgC;AAAA,qBAC5CkB,OAAOA,MAAP,KAAkB,eAAlB,IAAqC,CAACA,OAAOY,IADD;AAAA,aAAhC,CAAd;;AAGA9B,sBAAUpB,EAAEyC,GAAF,CAAMrB,OAAN,EAAe;AAAA,qBACvBpB,EAAEkC,QAAF,CAAWI,MAAX,EAAmB,EAAEA,QAAQ,MAAKjC,WAAL,CAAiB+B,OAAjB,CAAyBE,OAAOA,MAAhC,EAAwCrB,QAAQkC,UAAhD,EAA4D,aAA5D,CAAV,EAAnB,CADuB;AAAA,aAAf,CAAV;;AAGAJ,0BAAc3B,OAAd,GAAwBA,OAAxB;;AAEA,mBAAO2B,aAAP;AACD","file":"datasource.js","sourcesContent":["import _ from \"lodash\";\r\n\r\nexport class GenericDatasource {\r\n\r\n constructor(instanceSettings, $q, backendSrv, templateSrv) {\r\n this.type = instanceSettings.type;\r\n this.url = instanceSettings.url || \"\";\r\n var m = /con\\=(.*)/.exec(this.url.split(\"?\")[1]);\r\n this.connection = m ? m[1]: null;\r\n this.name = instanceSettings.name;\r\n this.q = $q;\r\n this.backendSrv = backendSrv;\r\n this.templateSrv = templateSrv;\r\n }\r\n\r\n buildRequest(rqtype, data){\r\n return { \r\n type: rqtype,\r\n body: data,\r\n url: this.connection \r\n };\r\n }\r\n\r\n query(options) {\r\n var query = this.buildQueryParameters(options);\r\n \r\n if (query.targets.length <= 0) {\r\n return this.q.when({data: []});\r\n }\r\n\r\n return this.backendSrv.datasourceRequest({\r\n url: this.url,\r\n data: this.buildRequest(\"query\", query ),\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' }\r\n });\r\n }\r\n\r\n testDatasource() {\r\n return this.backendSrv.datasourceRequest({\r\n url: this.url,\r\n method: 'POST',\r\n data: this.buildRequest(\"test\", null)\r\n }).then(result => {\r\n return { status: \"success\", message: \"Data source is working\", title: \"Success\" };\r\n }).catch(result => {\r\n return { status: \"error\", message: result, title: \"Error\" };\r\n });\r\n }\r\n\r\n annotationQuery(options) {\r\n var annotationQuery = _.assignIn({}, options);\r\n annotationQuery.annotation.query = this.templateSrv.replace(options.annotation.query, {}, 'glob'); \r\n \r\n return this.backendSrv.datasourceRequest({\r\n url: this.url,\r\n method: 'POST',\r\n data: this.buildRequest(\"annotations\", annotationQuery)\r\n }).then(result => {\r\n return result.data;\r\n });\r\n }\r\n\r\n metricFindQuery(options) {\r\n var opsAsString = typeof (options) === \"string\";\r\n if(options && options.type == 'sql')\r\n // TODO: Parser?\r\n return this.q.when([]);\r\n var target = opsAsString ? options : options.target;\r\n var interpolated = {\r\n target: this.templateSrv.replace(target, null, 'regex')\r\n };\r\n\r\n return this.backendSrv.datasourceRequest({\r\n url: this.url,\r\n data: this.buildRequest(\"search\", interpolated ),\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' }\r\n }).then(this.mapToTextValue);\r\n }\r\n\r\n mapToTextValue(result) {\r\n return _.map(result.data, (d, i) => {\r\n if (d && d.text && d.value) {\r\n return { text: d.text, value: d.value };\r\n } else if (_.isObject(d)) {\r\n return { text: d, value: i};\r\n }\r\n return { text: d, value: d };\r\n });\r\n }\r\n\r\n buildQueryParameters(options) {\r\n var clonedOptions = _.cloneDeep(options);\r\n var targets = _.filter(clonedOptions.targets, target => \r\n target.target !== 'select metric' && !target.hide);\r\n\r\n targets = _.map(targets, target => \r\n _.assignIn(target, { target: this.templateSrv.replace(target.target, options.scopedVars, \"distributed\")}));\r\n\r\n clonedOptions.targets = targets;\r\n\r\n return clonedOptions;\r\n }\r\n}\r\n"]}
--------------------------------------------------------------------------------
/dist/img/logo_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/dist/img/logo_large.png
--------------------------------------------------------------------------------
/dist/img/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/dist/img/logo_small.png
--------------------------------------------------------------------------------
/dist/module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | System.register(['./datasource', './query_ctrl'], function (_export, _context) {
4 | "use strict";
5 |
6 | var GenericDatasource, GenericDatasourceQueryCtrl, GenericConfigCtrl, GenericQueryOptionsCtrl, GenericAnnotationsQueryCtrl;
7 |
8 | function _classCallCheck(instance, Constructor) {
9 | if (!(instance instanceof Constructor)) {
10 | throw new TypeError("Cannot call a class as a function");
11 | }
12 | }
13 |
14 | return {
15 | setters: [function (_datasource) {
16 | GenericDatasource = _datasource.GenericDatasource;
17 | }, function (_query_ctrl) {
18 | GenericDatasourceQueryCtrl = _query_ctrl.GenericDatasourceQueryCtrl;
19 | }],
20 | execute: function () {
21 | _export('ConfigCtrl', GenericConfigCtrl = function GenericConfigCtrl() {
22 | _classCallCheck(this, GenericConfigCtrl);
23 | });
24 |
25 | GenericConfigCtrl.templateUrl = 'partials/config.html';
26 |
27 | _export('QueryOptionsCtrl', GenericQueryOptionsCtrl = function GenericQueryOptionsCtrl() {
28 | _classCallCheck(this, GenericQueryOptionsCtrl);
29 | });
30 |
31 | GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html';
32 |
33 | _export('AnnotationsQueryCtrl', GenericAnnotationsQueryCtrl = function GenericAnnotationsQueryCtrl() {
34 | _classCallCheck(this, GenericAnnotationsQueryCtrl);
35 | });
36 |
37 | GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html';
38 |
39 | _export('Datasource', GenericDatasource);
40 |
41 | _export('QueryCtrl', GenericDatasourceQueryCtrl);
42 |
43 | _export('ConfigCtrl', GenericConfigCtrl);
44 |
45 | _export('QueryOptionsCtrl', GenericQueryOptionsCtrl);
46 |
47 | _export('AnnotationsQueryCtrl', GenericAnnotationsQueryCtrl);
48 | }
49 | };
50 | });
51 | //# sourceMappingURL=module.js.map
52 |
--------------------------------------------------------------------------------
/dist/module.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../src/module.js"],"names":["GenericDatasource","GenericDatasourceQueryCtrl","GenericConfigCtrl","templateUrl","GenericQueryOptionsCtrl","GenericAnnotationsQueryCtrl"],"mappings":";;;;;;;;;;;;;;;AAAQA,uB,eAAAA,iB;;AACAC,gC,eAAAA,0B;;;4BAEFC,iB;;;;AACNA,wBAAkBC,WAAlB,GAAgC,sBAAhC;;kCAEMC,uB;;;;AACNA,8BAAwBD,WAAxB,GAAsC,6BAAtC;;sCAEME,2B;;;;AACNA,kCAA4BF,WAA5B,GAA0C,kCAA1C;;4BAGEH,iB;;2BACAC,0B;;4BACAC,iB;;kCACAE,uB;;sCACAC,2B","file":"module.js","sourcesContent":["import {GenericDatasource} from './datasource';\r\nimport {GenericDatasourceQueryCtrl} from './query_ctrl';\r\n\r\nclass GenericConfigCtrl {}\r\nGenericConfigCtrl.templateUrl = 'partials/config.html';\r\n\r\nclass GenericQueryOptionsCtrl {}\r\nGenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html';\r\n\r\nclass GenericAnnotationsQueryCtrl {}\r\nGenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html'\r\n\r\nexport {\r\n GenericDatasource as Datasource,\r\n GenericDatasourceQueryCtrl as QueryCtrl,\r\n GenericConfigCtrl as ConfigCtrl,\r\n GenericQueryOptionsCtrl as QueryOptionsCtrl,\r\n GenericAnnotationsQueryCtrl as AnnotationsQueryCtrl\r\n};\r\n"]}
--------------------------------------------------------------------------------
/dist/partials/annotations.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
Query
3 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/dist/partials/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/dist/partials/query.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
51 |
52 |
--------------------------------------------------------------------------------
/dist/partials/query.options.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SimpleSql",
3 | "id": "grafana-simple-sql-datasource",
4 | "type": "datasource",
5 |
6 | "partials": {
7 | "config": "public/app/plugins/datasource/simplesql/partials/config.html"
8 | },
9 |
10 | "metrics": true,
11 | "annotations": true,
12 |
13 | "info": {
14 | "description": "simple sql datasource",
15 | "author": {
16 | "name": "GBrian.",
17 | "url": ""
18 | },
19 | "logos": {
20 | "small": "img/logo_small.png",
21 | "large": "img/logo_large.png"
22 | },
23 | "links": [
24 | {"name": "GitHub", "url": "https://github.com/gbrian/grafana-simple-sql-datasource"},
25 | {"name": "MIT License", "url": "https://github.com/gbrian/grafana-simple-sql-datasource/blob/master/LICENSE"}
26 | ],
27 | "version": "0.0.2",
28 | "updated": "2017-02-14"
29 | },
30 |
31 | "dependencies": {
32 | "grafanaVersion": "3.x.x",
33 | "plugins": [ ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/dist/query_ctrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | System.register(['app/plugins/sdk', './css/query-editor.css!'], function (_export, _context) {
4 | "use strict";
5 |
6 | var QueryCtrl, _createClass, GenericDatasourceQueryCtrl;
7 |
8 | function _classCallCheck(instance, Constructor) {
9 | if (!(instance instanceof Constructor)) {
10 | throw new TypeError("Cannot call a class as a function");
11 | }
12 | }
13 |
14 | function _possibleConstructorReturn(self, call) {
15 | if (!self) {
16 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
17 | }
18 |
19 | return call && (typeof call === "object" || typeof call === "function") ? call : self;
20 | }
21 |
22 | function _inherits(subClass, superClass) {
23 | if (typeof superClass !== "function" && superClass !== null) {
24 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
25 | }
26 |
27 | subClass.prototype = Object.create(superClass && superClass.prototype, {
28 | constructor: {
29 | value: subClass,
30 | enumerable: false,
31 | writable: true,
32 | configurable: true
33 | }
34 | });
35 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
36 | }
37 |
38 | return {
39 | setters: [function (_appPluginsSdk) {
40 | QueryCtrl = _appPluginsSdk.QueryCtrl;
41 | }, function (_cssQueryEditorCss) {}],
42 | execute: function () {
43 | _createClass = function () {
44 | function defineProperties(target, props) {
45 | for (var i = 0; i < props.length; i++) {
46 | var descriptor = props[i];
47 | descriptor.enumerable = descriptor.enumerable || false;
48 | descriptor.configurable = true;
49 | if ("value" in descriptor) descriptor.writable = true;
50 | Object.defineProperty(target, descriptor.key, descriptor);
51 | }
52 | }
53 |
54 | return function (Constructor, protoProps, staticProps) {
55 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
56 | if (staticProps) defineProperties(Constructor, staticProps);
57 | return Constructor;
58 | };
59 | }();
60 |
61 | _export('GenericDatasourceQueryCtrl', GenericDatasourceQueryCtrl = function (_QueryCtrl) {
62 | _inherits(GenericDatasourceQueryCtrl, _QueryCtrl);
63 |
64 | function GenericDatasourceQueryCtrl($scope, $injector, uiSegmentSrv) {
65 | _classCallCheck(this, GenericDatasourceQueryCtrl);
66 |
67 | var _this = _possibleConstructorReturn(this, (GenericDatasourceQueryCtrl.__proto__ || Object.getPrototypeOf(GenericDatasourceQueryCtrl)).call(this, $scope, $injector));
68 |
69 | _this.scope = $scope;
70 | _this.uiSegmentSrv = uiSegmentSrv;
71 | _this.target.target = _this.target.target || 'select metric';
72 | _this.target.type = _this.target.type || 'timeserie';
73 | _this.target.cmdtype = _this.target.cmdtype || 'sql';
74 | _this.target.utc = _this.target.utc || 'localtime';
75 | return _this;
76 | }
77 |
78 | _createClass(GenericDatasourceQueryCtrl, [{
79 | key: 'getOptions',
80 | value: function getOptions() {
81 | return this.datasource.metricFindQuery(this.target).then(this.uiSegmentSrv.transformToSegments(false));
82 | // Options have to be transformed by uiSegmentSrv to be usable by metric-segment-model directive
83 | }
84 | }, {
85 | key: 'toggleEditorMode',
86 | value: function toggleEditorMode() {
87 | this.target.rawQuery = !this.target.rawQuery;
88 | }
89 | }, {
90 | key: 'onChangeInternal',
91 | value: function onChangeInternal() {
92 | this.panelCtrl.refresh(); // Asks the panel to refresh data.
93 | }
94 | }]);
95 |
96 | return GenericDatasourceQueryCtrl;
97 | }(QueryCtrl));
98 |
99 | _export('GenericDatasourceQueryCtrl', GenericDatasourceQueryCtrl);
100 |
101 | GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';
102 | }
103 | };
104 | });
105 | //# sourceMappingURL=query_ctrl.js.map
106 |
--------------------------------------------------------------------------------
/dist/query_ctrl.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../src/query_ctrl.js"],"names":["QueryCtrl","GenericDatasourceQueryCtrl","$scope","$injector","uiSegmentSrv","scope","target","type","cmdtype","utc","datasource","metricFindQuery","then","transformToSegments","rawQuery","panelCtrl","refresh","templateUrl"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAQA,e,kBAAAA,S;;;;;;;;;;;;;;;;;;;;;4CAGKC,0B;;;AAEX,4CAAYC,MAAZ,EAAoBC,SAApB,EAA+BC,YAA/B,EAA8C;AAAA;;AAAA,8JACtCF,MADsC,EAC9BC,SAD8B;;AAG5C,gBAAKE,KAAL,GAAaH,MAAb;AACA,gBAAKE,YAAL,GAAoBA,YAApB;AACA,gBAAKE,MAAL,CAAYA,MAAZ,GAAqB,MAAKA,MAAL,CAAYA,MAAZ,IAAsB,eAA3C;AACA,gBAAKA,MAAL,CAAYC,IAAZ,GAAmB,MAAKD,MAAL,CAAYC,IAAZ,IAAoB,WAAvC;AACA,gBAAKD,MAAL,CAAYE,OAAZ,GAAsB,MAAKF,MAAL,CAAYE,OAAZ,IAAuB,KAA7C;AACA,gBAAKF,MAAL,CAAYG,GAAZ,GAAkB,MAAKH,MAAL,CAAYG,GAAZ,IAAmB,WAArC;AAR4C;AAS7C;;;;uCAEY;AACX,mBAAO,KAAKC,UAAL,CAAgBC,eAAhB,CAAgC,KAAKL,MAArC,EACJM,IADI,CACC,KAAKR,YAAL,CAAkBS,mBAAlB,CAAsC,KAAtC,CADD,CAAP;AAEE;AACH;;;6CAEkB;AACjB,iBAAKP,MAAL,CAAYQ,QAAZ,GAAuB,CAAC,KAAKR,MAAL,CAAYQ,QAApC;AACD;;;6CAEkB;AACjB,iBAAKC,SAAL,CAAeC,OAAf,GADiB,CACS;AAC3B;;;;QAzB6ChB,S;;;;AA4BhDC,iCAA2BgB,WAA3B,GAAyC,4BAAzC","file":"query_ctrl.js","sourcesContent":["import {QueryCtrl} from 'app/plugins/sdk';\r\nimport './css/query-editor.css!'\r\n\r\nexport class GenericDatasourceQueryCtrl extends QueryCtrl {\r\n\r\n constructor($scope, $injector, uiSegmentSrv) {\r\n super($scope, $injector);\r\n\r\n this.scope = $scope;\r\n this.uiSegmentSrv = uiSegmentSrv;\r\n this.target.target = this.target.target || 'select metric';\r\n this.target.type = this.target.type || 'timeserie';\r\n this.target.cmdtype = this.target.cmdtype || 'sql';\r\n this.target.utc = this.target.utc || 'localtime';\r\n }\r\n\r\n getOptions() {\r\n return this.datasource.metricFindQuery(this.target)\r\n .then(this.uiSegmentSrv.transformToSegments(false));\r\n // Options have to be transformed by uiSegmentSrv to be usable by metric-segment-model directive\r\n }\r\n\r\n toggleEditorMode() {\r\n this.target.rawQuery = !this.target.rawQuery;\r\n }\r\n\r\n onChangeInternal() {\r\n this.panelCtrl.refresh(); // Asks the panel to refresh data.\r\n }\r\n}\r\n\r\nGenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';\r\n\r\n"]}
--------------------------------------------------------------------------------
/dist/serverside/drilldriver.js:
--------------------------------------------------------------------------------
1 | // https://drill.apache.org/docs/rest-api/
2 |
3 | var q = require("q");
4 | var Client = require('node-rest-client').Client;
5 |
6 | var client = new Client();
7 |
8 | function drilldriver(options){
9 | this.options = options;
10 | }
11 |
12 | drilldriver.prototype.buildQuery = function(cmd, parameters){
13 | if(parameters){
14 | var re = /[@$]([a-z0-9A-Z]*)/g;
15 | var m = null;
16 | while((m = re.exec(cmd)))
17 | if(parameters.hasOwnProperty(m[1]))
18 | cmd = cmd.replace(m[0], parameters[m[1]]);
19 | }
20 | return q.resolve(cmd);
21 | }
22 |
23 | drilldriver.prototype.get = function(url){
24 | var defer = q.defer();
25 | var req = client.get(this.options.url + url, defer.resolve);
26 | req.on('error', defer.reject);
27 | return defer.promise;
28 | }
29 |
30 | drilldriver.prototype.post = function(url, data){
31 | var defer = q.defer();
32 | var req = client.post(this.options.url + url, data, defer.resolve);
33 | req.on('error', defer.reject);
34 | return defer.promise;
35 | }
36 |
37 | drilldriver.prototype.connect = function(url){
38 | return this.get("/options.json");
39 | }
40 |
41 | drilldriver.prototype.query = function(command, parameters){
42 | var driver = this;
43 | return this.buildQuery(command, parameters)
44 | .then(sql => driver.post("/query.json", {"queryType" : "SQL", "query" : sql}));
45 | }
46 |
47 | drilldriver.prototype.parseResults = function(results){
48 | if(!results || !results.rows.length)
49 | return {};
50 | // Try figure out types
51 | /*const typeCheck {
52 | number: function(n){
53 |
54 | }
55 | };*/
56 | var frow = results.rows[0];
57 | var types = Object.keys(frow).map(key => {
58 | frow[key]
59 | return "string";
60 | });
61 | // results.columns = results.columns.map(c => { reutrn {}})
62 | return results;
63 | }
64 |
65 | module.exports = drilldriver;
--------------------------------------------------------------------------------
/dist/serverside/mssqldriver.js:
--------------------------------------------------------------------------------
1 | var mssql = require("mssql");
2 | var q = require("q");
3 |
4 | function mssqldriver(options){
5 | this.options = options;
6 | }
7 |
8 | mssqldriver.prototype.buildQuery = function(cmd, parameters){
9 | if(parameters){
10 | var re = /[@$]([a-z0-9A-Z]*)/g;
11 | var m = null;
12 | while((m = re.exec(cmd)))
13 | if(parameters.hasOwnProperty(m[1]))
14 | cmd = cmd.replace(m[0], parameters[m[1]]);
15 | }
16 | return q.resolve(cmd);
17 | }
18 |
19 | mssqldriver.prototype.connect = function(url){
20 | return mssql.connect(url || this.options.url);
21 | }
22 |
23 | mssqldriver.prototype.query = function(command, parameters){
24 | var defer = q.defer();
25 | var driver = this;
26 | this.buildQuery(command, parameters)
27 | .then(sql => this.connect()
28 | .then(conn => new mssql.Request(conn).query(sql, (err, results) =>
29 | defer[err ? "reject" : "resolve"](err || driver.parseResults(results))
30 | ))
31 | )
32 | .catch(defer.reject);
33 | return defer.promise;
34 | }
35 |
36 | mssqldriver.prototype.parseResults = function(results){
37 | return results ? {
38 | columns: results.columns,
39 | rows: results.map(r => r)
40 | }:{};
41 | }
42 |
43 | module.exports = mssqldriver;
--------------------------------------------------------------------------------
/dist/serverside/sqlproxyserver.js:
--------------------------------------------------------------------------------
1 | var _ = require("lodash");
2 | var q = require("q");
3 | var moment = require("moment");
4 |
5 | function SQLProxyServer(providerFactory) {
6 | this.providerFactory = providerFactory || this.defaultProviderFactory;
7 | }
8 |
9 | SQLProxyServer.prototype.defaultProviderFactory = function(url){
10 | var m = /([^:]*).*/.exec(url);
11 | if(!m) return q.reject("Invalid connection " + url);
12 | try{
13 | return q.resolve(require('./' + m[1] + "driver.js"));
14 | }catch(e){
15 | return q.reject(e);
16 | }
17 | }
18 |
19 | SQLProxyServer.prototype.loadProvider = function(req){
20 | return this.providerFactory(req.url)
21 | .then(provider => {
22 | req.provider = new provider(req);
23 | return req;
24 | });
25 | }
26 |
27 | SQLProxyServer.prototype.loadAPI = function(req){
28 | return this.loadProvider(req)
29 | .then(d => new this.API(req));
30 | }
31 |
32 | SQLProxyServer.prototype.execCommand = function(req){
33 | return this.loadAPI(req)
34 | .then(api => api.execute());
35 | }
36 | SQLProxyServer.prototype.getConnection = function(req){
37 | var consrc = [/con\=(.*)/.exec(req.url)[1], req.query.con, req.body.url];
38 | req.body.url = consrc.filter(s => s)[0];
39 | }
40 | SQLProxyServer.prototype.runStandalone = function(){
41 | var express = require('express');
42 | var bodyParser = require('body-parser');
43 | var app = express();
44 |
45 | app.use((req, res, next)=>{
46 | res.setHeader('Access-Control-Allow-Origin', '*');
47 | res.setHeader('Access-Control-Allow-Headers', 'Origin, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Response-Time, X-PINGOTHER, X-CSRF-Token,Authorization');
48 | res.setHeader('Access-Control-Allow-Methods', '*');
49 | res.setHeader('Access-Control-Expose-Headers', 'X-Api-Version, X-Request-Id, X-Response-Time');
50 | res.setHeader('Access-Control-Max-Age', '1000');
51 | return next();
52 | });
53 |
54 | app.use(bodyParser.json({
55 | strict: false,
56 | type: '*/*'
57 | }));
58 | var oThis = this;
59 | app.get("/", function(req, res){
60 | oThis.getConnection(req);
61 | if(!req.query.con){
62 | return res.status(500).send("Missing parameter `con` with connection details. "
63 | + "Example: con=mssql://dbuser:dbpwd@sqlserver/databasename");
64 | }
65 | oThis.execCommand({url: req.query.con, type:'test'})
66 | .then(data => res.send(data))
67 | .catch(err =>
68 | res.status(500).send(err));
69 | });
70 | app.post("/*", function(req, res){
71 | oThis.getConnection(req);
72 | oThis.execCommand(req.body)
73 | .then(data => res.send(data))
74 | .catch(err =>
75 | res.status(500).send(err));
76 | });
77 |
78 | var port = process.argv[2] || 666;
79 | app.listen(port);
80 | console.log("Server is listening to port " + port);
81 | }
82 |
83 | SQLProxyServer.prototype.API = function(command){
84 | var _api = this;
85 | _api.cmd = command;
86 |
87 | _api.execute = function(){
88 | return (_api[_api.cmd.type] || _api.default)();
89 | }
90 |
91 | _api.default = function(){
92 | return q.reject("Invalid command " + _api.cmd.type);
93 | }
94 |
95 | _api.test = function(){
96 | return _api.cmd.provider.query("SELECT 1;")
97 | .then(r => {return{status:'sucess'}})
98 | .catch(_api.internals.error);
99 | };
100 |
101 | _api.query = function(){
102 | var queries =
103 | _api.cmd.body.targets.map(t => {
104 | return _api.cmd.provider.query(t.target, _.assignIn({}, _api.cmd.body.range, t))
105 | .then(r =>
106 | _api.internals.parse(_api.cmd, t, r).results
107 | )});
108 | return q.all(queries)
109 | .then(_.concat)
110 | .then(_.flatten);
111 | }
112 |
113 | _api.search = function(){
114 | /**
115 | * Example array response
116 | * ["upper_25","upper_50","upper_75","upper_90","upper_95"]
117 | * Example map response
118 | * [ { "text" :"upper_25", "value": 1}, { "text" :"upper_75", "value": 2} ]
119 | */
120 | _api.cmd.body.targets = [{
121 | target: _api.cmd.body.target,
122 | type: 'search'
123 | }];
124 | return _api.query();
125 | }
126 |
127 | _api.annotations = function(){
128 | _api.cmd.body.targets = [{
129 | target: _api.cmd.body.annotation.query,
130 | annotation: _api.cmd.body.annotation,
131 | type: 'annotations'
132 | }];
133 | return _api.query();
134 | }
135 |
136 | _api.internals = {
137 | try: (fnc)=>{
138 | try{
139 | return fnc();
140 | }catch(e){
141 | console.error(e);
142 | return e;
143 | }
144 | },
145 | parse: (reqData, target, results)=>{
146 | if(target.type == "timeseries")
147 | return _api.internals.try(() => _api.internals.parseTimeseries(target, results));
148 | if(target.type == "table")
149 | return _api.internals.try(() => _api.internals.parseTable(target, results));
150 | if(target.type == "annotations")
151 | return _api.internals.try(() => _api.internals.parseAnnotations(target, results));
152 | if(target.type == "search")
153 | return _api.internals.try(() => _api.internals.parseSearch(target, results));
154 | return "Unsupported response type: " + target.type;
155 | },
156 | parseSearch: (target, results) => {
157 | var mapped = Object.keys(results.columns).length > 1;
158 | if(mapped){
159 | var textColumn = _api.internals.getColumn('text', target, results);
160 | var valueColumn = _api.internals.getColumn('value', target, results);
161 | target.results = results.rows.map(r => ({ "text" :r[textColumn], "value": r[valueColumn]}));
162 | }else{
163 | var col = Object.keys(results.columns)[0];
164 | target.results = results.rows.map(r => r[col]);
165 | }
166 | return target;
167 | },
168 | parseAnnotations: (target, results) => {
169 | target.timestamp = _api.internals.getTimestamp(target, results);
170 | target.results = results.rows.map(r => _.assignIn(r, {
171 | annotation: target.annotation,
172 | time: _api.internals.utc(r[target.timestamp], target.annotation.utc).valueOf()
173 | }));
174 | return target;
175 | },
176 | utc: function(value, utc){
177 | if(utc == 'localtime' && (value && value.getTimezoneOffset)){
178 | var dateTime = moment(value);
179 | return dateTime.add(value.getTimezoneOffset(), 'm');
180 | }
181 | return value;
182 | },
183 | parseTimeseries: (target, results)=>{
184 | target.timestamp = _api.internals.getTimestamp(target, results);
185 | target.metric = _api.internals.getMetric(target, results);
186 | target.value = _api.internals.getMetricValue(target, results);
187 | target.results = _.map(
188 | _.groupBy(results.rows, target.metric),
189 | (v, k) => {
190 | return {
191 | target: k,
192 | datapoints: v.map(r => [r[target.value], _api.internals.utc(r[target.timestamp], target.utc).valueOf()])
193 | };
194 | });
195 | return target;
196 | },
197 | parseTable: (target, results)=>{
198 | var mapType = function(sqltype){
199 | if(_.filter(['int', 'byte', 'decimal', 'float', 'double', 'money', 'bit',
200 | 'numeric', 'real'],
201 | (sqlt) => sqltype.indexOf(sqlt) != -1).length != 0)
202 | return "number";
203 | if(_.filter(['date', 'time'],
204 | (sqlt) => sqltype.indexOf(sqlt) != -1).length != 0)
205 | return "time";
206 | return "string";
207 | }
208 | var columns = _.map(results.columns, (v,k) =>{
209 | return {text:k, type:mapType(v.type.name.toLowerCase()), sqltype: v.type.name};
210 | });
211 | var rows = _.map(results.rows, (r) => _.map(r, (v) => v));
212 | target.results = {columns: columns, rows: rows, type:'table'};
213 | return target;
214 | },
215 | getTimestamp: (target, results)=>{
216 | return _api.internals.getSpecialColumn(results, (k, type) =>
217 | k == target.timestampKey ? 1000:
218 | k.toLowerCase() == 'timestamp' ? 100:
219 | results.columns[k].type.name == 'DateTime' ? 1: 0)
220 | },
221 | getMetric: (target, results)=>{
222 | return _api.internals.getSpecialColumn(results, (k, type) =>
223 | k == target.metric ? 1000:
224 | ['metric', 'key'].indexOf(k.toLowerCase()) != -1 ? 100:
225 | type == 'text' || type.indexOf('char') != -1 ? 1: 0)
226 | },
227 | getMetricValue: (target, results)=>{
228 | return _api.internals.getSpecialColumn(results, (k, type) =>
229 | k == target.value ? 1000:
230 | k.toLowerCase() == 'value' ? 100:
231 | [target.timestamp, target.metric].indexOf(k.toLowerCase()) == -1 ? 1: 0)
232 | },
233 | getColumn: (column, target, results)=>{
234 | return _api.internals.getSpecialColumn(results, (k, type) =>
235 | k == target.metric ? 1000:
236 | [column].indexOf(k.toLowerCase()) != -1 ? 100:
237 | type == 'text' || type.indexOf('char') != -1 ? 1: 0)
238 | },
239 | getSpecialColumn: (results, score)=>{
240 | return _.orderBy(Object.keys(results.columns)
241 | .map(k => {
242 | var type = results.columns[k].type.name.toLowerCase();
243 | return { key: k, score: score(k, type) }
244 | }),
245 | ['score'],
246 | ['desc'])[0].key
247 | }
248 | }
249 | return this;
250 | }
251 |
252 | if (require.main === module) {
253 | new SQLProxyServer().runStandalone();
254 | } else {
255 | module.exports = SQLProxyServer;
256 | }
257 |
258 |
--------------------------------------------------------------------------------
/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/overview.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grafana-simple-sql-datasource",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/gbrian/grafana-simple-sql-datasource.git"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "bugs": {
17 | "url": "https://github.com/gbrian/grafana-simple-sql-datasource/issues"
18 | },
19 | "devDependencies": {
20 | "grunt": "~0.4.5",
21 | "babel": "~6.5.1",
22 | "grunt-babel": "~6.0.0",
23 | "grunt-contrib-copy": "~0.8.2",
24 | "grunt-contrib-watch": "^0.6.1",
25 | "grunt-contrib-uglify": "~0.11.0",
26 | "grunt-mocha-test": "~0.12.7",
27 | "grunt-systemjs-builder": "^0.2.5",
28 | "load-grunt-tasks": "~3.2.0",
29 | "grunt-execute": "~0.2.2",
30 | "grunt-contrib-clean": "~0.6.0",
31 | "prunk": "~1.2.1",
32 | "q": "^1.4.1",
33 | "chai": "~3.5.0"
34 | },
35 | "dependencies": {
36 | "babel-plugin-transform-es2015-for-of": "^6.6.0",
37 | "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0",
38 | "babel-preset-es2015": "^6.5.0",
39 | "body-parser": "^1.16.0",
40 | "express": "^4.14.1",
41 | "express-cors": "0.0.3",
42 | "lodash": "^4.0.1",
43 | "mocha": "^2.4.5",
44 | "moment": "^2.17.1",
45 | "mssql": "^3.3.0",
46 | "node-rest-client": "^2.5.0",
47 | "q": "^1.4.1"
48 | },
49 | "homepage": "https://github.com/gbrian/grafana-simple-sql-datasource#readme"
50 | }
51 |
--------------------------------------------------------------------------------
/query_editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/query_editor.png
--------------------------------------------------------------------------------
/spec/datasource_spec.js:
--------------------------------------------------------------------------------
1 | import {Datasource} from "../module";
2 | import Q from "q";
3 |
4 | describe('GenericDatasource', function() {
5 | var ctx = {};
6 |
7 | beforeEach(function() {
8 | ctx.$q = Q;
9 | ctx.backendSrv = {};
10 | ctx.templateSrv = {};
11 | ctx.ds = new Datasource({}, ctx.$q, ctx.backendSrv, ctx.templateSrv);
12 |
13 | });
14 |
15 | it('should return an empty array when no targets are set', function(done) {
16 | ctx.ds.query({targets: []}).then(function(result) {
17 | expect(result.data).to.have.length(0);
18 | done();
19 | });
20 | });
21 |
22 | it('should return the server results when a target is set', function(done) {
23 | ctx.backendSrv.datasourceRequest = function(request) {
24 | request.data = request.data.body;
25 | return ctx.$q.when({
26 | _request: request,
27 | data: [
28 | {
29 | target: 'X',
30 | datapoints: [1, 2, 3]
31 | }
32 | ]
33 | });
34 | };
35 |
36 | ctx.templateSrv.replace = function(data) {
37 | return data;
38 | }
39 |
40 | ctx.ds.query({targets: ['hits']}).then(function(result) {
41 | expect(result._request.data.targets).to.have.length(1);
42 |
43 | var series = result.data[0];
44 | expect(series.target).to.equal('X');
45 | expect(series.datapoints).to.have.length(3);
46 | done();
47 | });
48 | });
49 |
50 | it ('should return the metric results when a target is null', function(done) {
51 | ctx.backendSrv.datasourceRequest = function(request) {
52 | return ctx.$q.when({
53 | _request: request,
54 | data: [
55 | "metric_0",
56 | "metric_1",
57 | "metric_2",
58 | ]
59 | });
60 | };
61 |
62 | ctx.templateSrv.replace = function(data) {
63 | return data;
64 | }
65 |
66 | ctx.ds.metricFindQuery({target: null}).then(function(result) {
67 | expect(result).to.have.length(3);
68 | expect(result[0].text).to.equal('metric_0');
69 | expect(result[0].value).to.equal('metric_0');
70 | expect(result[1].text).to.equal('metric_1');
71 | expect(result[1].value).to.equal('metric_1');
72 | expect(result[2].text).to.equal('metric_2');
73 | expect(result[2].value).to.equal('metric_2');
74 | done();
75 | });
76 | });
77 |
78 | it ('should return the metric target results when a target is set', function(done) {
79 | ctx.backendSrv.datasourceRequest = function(request) {
80 | request.data = request.data.body;
81 | var target = request.data.target;
82 | var result = [target + "_0", target + "_1", target + "_2"];
83 |
84 | return ctx.$q.when({
85 | _request: request,
86 | data: result
87 | });
88 | };
89 |
90 | ctx.templateSrv.replace = function(data) {
91 | return data;
92 | }
93 |
94 | ctx.ds.metricFindQuery({target: 'search'}).then(function(result) {
95 | expect(result).to.have.length(3);
96 | expect(result[0].text).to.equal('search_0');
97 | expect(result[0].value).to.equal('search_0');
98 | expect(result[1].text).to.equal('search_1');
99 | expect(result[1].value).to.equal('search_1');
100 | expect(result[2].text).to.equal('search_2');
101 | expect(result[2].value).to.equal('search_2');
102 | done();
103 | });
104 | });
105 |
106 | it ('should return the metric results when the target is an empty string', function(done) {
107 | ctx.backendSrv.datasourceRequest = function(request) {
108 | return ctx.$q.when({
109 | _request: request,
110 | data: [
111 | "metric_0",
112 | "metric_1",
113 | "metric_2",
114 | ]
115 | });
116 | };
117 |
118 | ctx.templateSrv.replace = function(data) {
119 | return data;
120 | }
121 |
122 | ctx.ds.metricFindQuery({target: ''}).then(function(result) {
123 | expect(result).to.have.length(3);
124 | expect(result[0].text).to.equal('metric_0');
125 | expect(result[0].value).to.equal('metric_0');
126 | expect(result[1].text).to.equal('metric_1');
127 | expect(result[1].value).to.equal('metric_1');
128 | expect(result[2].text).to.equal('metric_2');
129 | expect(result[2].value).to.equal('metric_2');
130 | done();
131 | });
132 | });
133 |
134 | it ('should return the metric results when the args are an empty object', function(done) {
135 | ctx.backendSrv.datasourceRequest = function(request) {
136 | return ctx.$q.when({
137 | _request: request,
138 | data: [
139 | "metric_0",
140 | "metric_1",
141 | "metric_2",
142 | ]
143 | });
144 | };
145 |
146 | ctx.templateSrv.replace = function(data) {
147 | return data;
148 | }
149 |
150 | ctx.ds.metricFindQuery({}).then(function(result) {
151 | expect(result).to.have.length(3);
152 | expect(result[0].text).to.equal('metric_0');
153 | expect(result[0].value).to.equal('metric_0');
154 | expect(result[1].text).to.equal('metric_1');
155 | expect(result[1].value).to.equal('metric_1');
156 | expect(result[2].text).to.equal('metric_2');
157 | expect(result[2].value).to.equal('metric_2');
158 | done();
159 | });
160 | });
161 |
162 | it ('should throw error when args are undefined', function(done) {
163 | global.assert.throw(ctx.ds.metricFindQuery, Error, "Cannot read property 'target' of undefined");
164 | done();
165 | });
166 |
167 | it ('should throw error when args are null', function(done) {
168 | global.assert.throw(function() { ctx.ds.metricFindQuery(null); }, Error, "Cannot read property 'target' of null");
169 | done();
170 | });
171 |
172 | it ('should return the metric target results when the args are a string', function(done) {
173 | ctx.backendSrv.datasourceRequest = function(request) {
174 | request.data = request.data.body;
175 | var target = request.data.target;
176 | var result = [target + "_0", target + "_1", target + "_2"];
177 |
178 | return ctx.$q.when({
179 | _request: request,
180 | data: result
181 | });
182 | };
183 |
184 | ctx.templateSrv.replace = function(data) {
185 | return data;
186 | }
187 |
188 | ctx.ds.metricFindQuery('search').then(function(result) {
189 | expect(result).to.have.length(3);
190 | expect(result[0].text).to.equal('search_0');
191 | expect(result[0].value).to.equal('search_0');
192 | expect(result[1].text).to.equal('search_1');
193 | expect(result[1].value).to.equal('search_1');
194 | expect(result[2].text).to.equal('search_2');
195 | expect(result[2].value).to.equal('search_2');
196 | done();
197 | });
198 | });
199 |
200 | it ('should return data as text and as value', function(done) {
201 | var result = ctx.ds.mapToTextValue({data: ["zero", "one", "two"]});
202 |
203 | expect(result).to.have.length(3);
204 | expect(result[0].text).to.equal('zero');
205 | expect(result[0].value).to.equal('zero');
206 | expect(result[1].text).to.equal('one');
207 | expect(result[1].value).to.equal('one');
208 | expect(result[2].text).to.equal('two');
209 | expect(result[2].value).to.equal('two');
210 | done();
211 | });
212 |
213 | it ('should return text as text and value as value', function(done) {
214 | var data = [
215 | {text: "zero", value: "value_0"},
216 | {text: "one", value: "value_1"},
217 | {text: "two", value: "value_2"},
218 | ];
219 |
220 | var result = ctx.ds.mapToTextValue({data: data});
221 |
222 | expect(result).to.have.length(3);
223 | expect(result[0].text).to.equal('zero');
224 | expect(result[0].value).to.equal('value_0');
225 | expect(result[1].text).to.equal('one');
226 | expect(result[1].value).to.equal('value_1');
227 | expect(result[2].text).to.equal('two');
228 | expect(result[2].value).to.equal('value_2');
229 | done();
230 | });
231 |
232 | it ('should return data as text and index as value', function(done) {
233 | var data = [
234 | {a: "zero", b: "value_0"},
235 | {a: "one", b: "value_1"},
236 | {a: "two", b: "value_2"},
237 | ];
238 |
239 | var result = ctx.ds.mapToTextValue({data: data});
240 |
241 | expect(result).to.have.length(3);
242 | expect(result[0].text).to.equal(data[0]);
243 | expect(result[0].value).to.equal(0);
244 | expect(result[1].text).to.equal(data[1]);
245 | expect(result[1].value).to.equal(1);
246 | expect(result[2].text).to.equal(data[2]);
247 | expect(result[2].value).to.equal(2);
248 | done();
249 | });
250 | });
251 |
--------------------------------------------------------------------------------
/spec/sqlproxyserver_spec.js:
--------------------------------------------------------------------------------
1 | // Loading dist version instead "test" because I have no idea about
2 | // how to prevent the test side to be "babelized"
3 | var SQLProxyServer = require("../../serverside/sqlproxyserver.js");
4 | import Q from "q";
5 | import _ from "lodash";
6 |
7 | describe('sqlproxyserver', function() {
8 | function mocksqldriver(options){
9 | this.options = options;
10 | }
11 | mocksqldriver.prototype.connect = function(){
12 | if(!this.options.url)
13 | return Q.reject("Invalid or empty connection string");
14 | return Q.resolve(this.options);
15 | };
16 | mocksqldriver.prototype.query = function(cmd){
17 | var results = {
18 | query:cmd,
19 | columns:{
20 | metric:{type:{name:'VarChar'}},
21 | value:{type:{name:'Int'}},
22 | timestamp:{type:{name:'DateTime'}}
23 | },
24 | rows:[{metric:'metric', value:1, timestamp:new Date()}]
25 | };
26 | return this.connect()
27 | .then(() => results);
28 | };
29 |
30 | var sqlproxyds = null;
31 | var query = {type:'query',url:'mssql://....',
32 | body: {
33 | targets:[{
34 | target: 'SELECT 1',
35 | type: 'timeseries'
36 | },{
37 | target: 'SELECT 1',
38 | type: 'table'
39 | }],
40 | range:{from: '',to: ''}}
41 | };
42 |
43 |
44 | beforeEach(function() {
45 | sqlproxyds = new SQLProxyServer(()=>Q.resolve(mocksqldriver));
46 | });
47 |
48 | it('should fail for empty connection string', function(done) {
49 | sqlproxyds.execCommand({type:'test'})
50 | .catch(err => done());
51 | });
52 |
53 | it('should test', function(done) {
54 | sqlproxyds.execCommand({type:'test', url:'mssql://....', body:{targets:[{}]}})
55 | .then(r => {
56 | assert(r.status == 'sucess', "Test failed");
57 | done();
58 | })
59 | .catch(done);
60 | });
61 |
62 | it('query shuld return timeseries', function(done) {
63 | sqlproxyds.execCommand(query)
64 | .then(r => {
65 | assert(r, "No data returned");
66 | assert(r.length == query.body.targets.length,
67 | "Wrong number of data set returned. Expected " + query.body.targets.length + " got " + r.length);
68 | var tsdt = r[0].datapoints;
69 | assert(tsdt[0][0] == 1, "Invalid data value returned");
70 | assert(new Date(tsdt[0][1]).getFullYear() == (new Date()).getFullYear(),
71 | "Invalid timestamp returned " + new Date(tsdt[0][1]));
72 | done();
73 | })
74 | .catch(done);
75 | });
76 | it('query shuld return table', function(done) {
77 | sqlproxyds.execCommand(query)
78 | .then(r => {
79 | assert(r, "No data returned");
80 | assert(r.length == query.body.targets.length,
81 | "Wrong number of data set returned. Expected " + query.body.targets.length + " got " + r.length);
82 | var tsdt = r[1];
83 | assert(tsdt.columns, "No columns found");
84 | assert(tsdt.rows, "No rows found");
85 | assert(tsdt.type == "table", "Inavlid type, espected 'table' found " + tsdt.type);
86 | assert(tsdt.columns.length != 0, "Invalid number of columns");
87 | assert(tsdt.columns.filter(c => ["number","string","time"].indexOf(c.type) == -1).length == 0, "Found invalid column type");
88 | assert(tsdt.columns.length == tsdt.rows[0].length, "Number fields don't not match number of columns");
89 | done();
90 | })
91 | .catch(done);
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/spec/test-main.js:
--------------------------------------------------------------------------------
1 | import prunk from 'prunk';
2 | import {jsdom} from 'jsdom';
3 | import chai from 'chai';
4 |
5 | // Mock Grafana modules that are not available outside of the core project
6 | // Required for loading module.js
7 | prunk.mock('./css/query-editor.css!', 'no css, dude.');
8 | prunk.mock('app/plugins/sdk', {
9 | QueryCtrl: null
10 | });
11 |
12 | // Setup jsdom
13 | // Required for loading angularjs
14 | global.document = jsdom('');
15 | global.window = global.document.parentWindow;
16 | global.navigator = window.navigator = {};
17 | global.Node = window.Node;
18 |
19 | // Setup Chai
20 | chai.should();
21 | global.assert = chai.assert;
22 | global.expect = chai.expect;
23 |
--------------------------------------------------------------------------------
/src/css/query-editor.css:
--------------------------------------------------------------------------------
1 | .grafana-simple-sql-datasource .hidden {
2 | display: none;
3 | }
4 |
5 | .ace-sql-editor{
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 | }
--------------------------------------------------------------------------------
/src/datasource.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export class GenericDatasource {
4 |
5 | constructor(instanceSettings, $q, backendSrv, templateSrv) {
6 | this.type = instanceSettings.type;
7 | this.url = instanceSettings.url || "";
8 | var m = /con\=(.*)/.exec(this.url.split("?")[1]);
9 | this.connection = m ? m[1]: null;
10 | this.name = instanceSettings.name;
11 | this.q = $q;
12 | this.backendSrv = backendSrv;
13 | this.templateSrv = templateSrv;
14 | }
15 |
16 | buildRequest(rqtype, data){
17 | return {
18 | type: rqtype,
19 | body: data,
20 | url: this.connection
21 | };
22 | }
23 |
24 | query(options) {
25 | var query = this.buildQueryParameters(options);
26 |
27 | if (query.targets.length <= 0) {
28 | return this.q.when({data: []});
29 | }
30 |
31 | return this.backendSrv.datasourceRequest({
32 | url: this.url,
33 | data: this.buildRequest("query", query ),
34 | method: 'POST',
35 | headers: { 'Content-Type': 'application/json' }
36 | });
37 | }
38 |
39 | testDatasource() {
40 | return this.backendSrv.datasourceRequest({
41 | url: this.url,
42 | method: 'POST',
43 | data: this.buildRequest("test", null)
44 | }).then(result => {
45 | return { status: "success", message: "Data source is working", title: "Success" };
46 | }).catch(result => {
47 | return { status: "error", message: result, title: "Error" };
48 | });
49 | }
50 |
51 | annotationQuery(options) {
52 | var annotationQuery = _.assignIn({}, options);
53 | annotationQuery.annotation.query = this.templateSrv.replace(options.annotation.query, {}, 'glob');
54 |
55 | return this.backendSrv.datasourceRequest({
56 | url: this.url,
57 | method: 'POST',
58 | data: this.buildRequest("annotations", annotationQuery)
59 | }).then(result => {
60 | return result.data;
61 | });
62 | }
63 |
64 | metricFindQuery(options) {
65 | var opsAsString = typeof (options) === "string";
66 | if(options && options.type == 'sql')
67 | // TODO: Parser?
68 | return this.q.when([]);
69 | var target = opsAsString ? options : options.target;
70 | var interpolated = {
71 | target: this.templateSrv.replace(target, null, 'regex')
72 | };
73 |
74 | return this.backendSrv.datasourceRequest({
75 | url: this.url,
76 | data: this.buildRequest("search", interpolated ),
77 | method: 'POST',
78 | headers: { 'Content-Type': 'application/json' }
79 | }).then(this.mapToTextValue);
80 | }
81 |
82 | mapToTextValue(result) {
83 | return _.map(result.data, (d, i) => {
84 | if (d && d.text && d.value) {
85 | return { text: d.text, value: d.value };
86 | } else if (_.isObject(d)) {
87 | return { text: d, value: i};
88 | }
89 | return { text: d, value: d };
90 | });
91 | }
92 |
93 | buildQueryParameters(options) {
94 | var clonedOptions = _.cloneDeep(options);
95 | var targets = _.filter(clonedOptions.targets, target =>
96 | target.target !== 'select metric' && !target.hide);
97 |
98 | targets = _.map(targets, target =>
99 | _.assignIn(target, { target: this.templateSrv.replace(target.target, options.scopedVars, "distributed")}));
100 |
101 | clonedOptions.targets = targets;
102 |
103 | return clonedOptions;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/img/logo_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/src/img/logo_large.png
--------------------------------------------------------------------------------
/src/img/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbrian/grafana-simple-sql-datasource/14967005e5e2cb3edd3bd1206c5e1aaf5608b51d/src/img/logo_small.png
--------------------------------------------------------------------------------
/src/module.js:
--------------------------------------------------------------------------------
1 | import {GenericDatasource} from './datasource';
2 | import {GenericDatasourceQueryCtrl} from './query_ctrl';
3 |
4 | class GenericConfigCtrl {}
5 | GenericConfigCtrl.templateUrl = 'partials/config.html';
6 |
7 | class GenericQueryOptionsCtrl {}
8 | GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html';
9 |
10 | class GenericAnnotationsQueryCtrl {}
11 | GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html'
12 |
13 | export {
14 | GenericDatasource as Datasource,
15 | GenericDatasourceQueryCtrl as QueryCtrl,
16 | GenericConfigCtrl as ConfigCtrl,
17 | GenericQueryOptionsCtrl as QueryOptionsCtrl,
18 | GenericAnnotationsQueryCtrl as AnnotationsQueryCtrl
19 | };
20 |
--------------------------------------------------------------------------------
/src/partials/annotations.editor.html:
--------------------------------------------------------------------------------
1 |
2 | Query
3 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/partials/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/partials/query.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
51 |
52 |
--------------------------------------------------------------------------------
/src/partials/query.options.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SimpleSql",
3 | "id": "grafana-simple-sql-datasource",
4 | "type": "datasource",
5 |
6 | "partials": {
7 | "config": "public/app/plugins/datasource/simplesql/partials/config.html"
8 | },
9 |
10 | "metrics": true,
11 | "annotations": true,
12 |
13 | "info": {
14 | "description": "simple sql datasource",
15 | "author": {
16 | "name": "GBrian.",
17 | "url": ""
18 | },
19 | "logos": {
20 | "small": "img/logo_small.png",
21 | "large": "img/logo_large.png"
22 | },
23 | "links": [
24 | {"name": "GitHub", "url": "https://github.com/gbrian/grafana-simple-sql-datasource"},
25 | {"name": "MIT License", "url": "https://github.com/gbrian/grafana-simple-sql-datasource/blob/master/LICENSE"}
26 | ],
27 | "version": "0.0.2",
28 | "updated": "2017-02-14"
29 | },
30 |
31 | "dependencies": {
32 | "grafanaVersion": "3.x.x",
33 | "plugins": [ ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/query_ctrl.js:
--------------------------------------------------------------------------------
1 | import {QueryCtrl} from 'app/plugins/sdk';
2 | import './css/query-editor.css!'
3 |
4 | export class GenericDatasourceQueryCtrl extends QueryCtrl {
5 |
6 | constructor($scope, $injector, uiSegmentSrv) {
7 | super($scope, $injector);
8 |
9 | this.scope = $scope;
10 | this.uiSegmentSrv = uiSegmentSrv;
11 | this.target.target = this.target.target || 'select metric';
12 | this.target.type = this.target.type || 'timeserie';
13 | this.target.cmdtype = this.target.cmdtype || 'sql';
14 | this.target.utc = this.target.utc || 'localtime';
15 | }
16 |
17 | getOptions() {
18 | return this.datasource.metricFindQuery(this.target)
19 | .then(this.uiSegmentSrv.transformToSegments(false));
20 | // Options have to be transformed by uiSegmentSrv to be usable by metric-segment-model directive
21 | }
22 |
23 | toggleEditorMode() {
24 | this.target.rawQuery = !this.target.rawQuery;
25 | }
26 |
27 | onChangeInternal() {
28 | this.panelCtrl.refresh(); // Asks the panel to refresh data.
29 | }
30 | }
31 |
32 | GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';
33 |
34 |
--------------------------------------------------------------------------------
/src/serverside/drilldriver.js:
--------------------------------------------------------------------------------
1 | // https://drill.apache.org/docs/rest-api/
2 |
3 | var q = require("q");
4 | var Client = require('node-rest-client').Client;
5 |
6 | var client = new Client();
7 |
8 | function drilldriver(options){
9 | this.options = options;
10 | }
11 |
12 | drilldriver.prototype.buildQuery = function(cmd, parameters){
13 | if(parameters){
14 | var re = /[@$]([a-z0-9A-Z]*)/g;
15 | var m = null;
16 | while((m = re.exec(cmd)))
17 | if(parameters.hasOwnProperty(m[1]))
18 | cmd = cmd.replace(m[0], parameters[m[1]]);
19 | }
20 | return q.resolve(cmd);
21 | }
22 |
23 | drilldriver.prototype.get = function(url){
24 | var defer = q.defer();
25 | var req = client.get(this.options.url + url, defer.resolve);
26 | req.on('error', defer.reject);
27 | return defer.promise;
28 | }
29 |
30 | drilldriver.prototype.post = function(url, data){
31 | var defer = q.defer();
32 | var req = client.post(this.options.url + url, data, defer.resolve);
33 | req.on('error', defer.reject);
34 | return defer.promise;
35 | }
36 |
37 | drilldriver.prototype.connect = function(url){
38 | return this.get("/options.json");
39 | }
40 |
41 | drilldriver.prototype.query = function(command, parameters){
42 | var driver = this;
43 | return this.buildQuery(command, parameters)
44 | .then(sql => driver.post("/query.json", {"queryType" : "SQL", "query" : sql}));
45 | }
46 |
47 | drilldriver.prototype.parseResults = function(results){
48 | if(!results || !results.rows.length)
49 | return {};
50 | // Try figure out types
51 | /*const typeCheck {
52 | number: function(n){
53 |
54 | }
55 | };*/
56 | var frow = results.rows[0];
57 | var types = Object.keys(frow).map(key => {
58 | frow[key]
59 | return "string";
60 | });
61 | // results.columns = results.columns.map(c => { reutrn {}})
62 | return results;
63 | }
64 |
65 | module.exports = drilldriver;
--------------------------------------------------------------------------------
/src/serverside/mssqldriver.js:
--------------------------------------------------------------------------------
1 | var mssql = require("mssql");
2 | var q = require("q");
3 |
4 | function mssqldriver(options){
5 | this.options = options;
6 | }
7 |
8 | mssqldriver.prototype.buildQuery = function(cmd, parameters){
9 | if(parameters){
10 | var re = /[@$]([a-z0-9A-Z]*)/g;
11 | var m = null;
12 | while((m = re.exec(cmd)))
13 | if(parameters.hasOwnProperty(m[1]))
14 | cmd = cmd.replace(m[0], parameters[m[1]]);
15 | }
16 | return q.resolve(cmd);
17 | }
18 |
19 | mssqldriver.prototype.connect = function(url){
20 | return mssql.connect(url || this.options.url);
21 | }
22 |
23 | mssqldriver.prototype.query = function(command, parameters){
24 | var defer = q.defer();
25 | var driver = this;
26 | this.buildQuery(command, parameters)
27 | .then(sql => this.connect()
28 | .then(conn => new mssql.Request(conn).query(sql, (err, results) =>
29 | defer[err ? "reject" : "resolve"](err || driver.parseResults(results))
30 | ))
31 | )
32 | .catch(defer.reject);
33 | return defer.promise;
34 | }
35 |
36 | mssqldriver.prototype.parseResults = function(results){
37 | return results ? {
38 | columns: results.columns,
39 | rows: results.map(r => r)
40 | }:{};
41 | }
42 |
43 | module.exports = mssqldriver;
--------------------------------------------------------------------------------
/src/serverside/sqlproxy.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=grafana-simple-sql-datasource
3 | After=network.target
4 |
5 | [Service]
6 | ExecStart=/usr/bin/node /var/lib/grafana/plugins/grafana-simple-sql-datasource-master/dist/serverside/sqlproxyserver.js
7 | Restart=on-failure
8 |
9 | [Install]
10 | WantedBy=multi-user.target
11 |
--------------------------------------------------------------------------------
/src/serverside/sqlproxyserver.js:
--------------------------------------------------------------------------------
1 | var _ = require("lodash");
2 | var q = require("q");
3 | var moment = require("moment");
4 |
5 | function SQLProxyServer(providerFactory) {
6 | this.providerFactory = providerFactory || this.defaultProviderFactory;
7 | }
8 |
9 | SQLProxyServer.prototype.defaultProviderFactory = function(url){
10 | var m = /([^:]*).*/.exec(url);
11 | if(!m) return q.reject("Invalid connection " + url);
12 | try{
13 | return q.resolve(require('./' + m[1] + "driver.js"));
14 | }catch(e){
15 | return q.reject(e);
16 | }
17 | }
18 |
19 | SQLProxyServer.prototype.loadProvider = function(req){
20 | return this.providerFactory(req.url)
21 | .then(provider => {
22 | req.provider = new provider(req);
23 | return req;
24 | });
25 | }
26 |
27 | SQLProxyServer.prototype.loadAPI = function(req){
28 | return this.loadProvider(req)
29 | .then(d => new this.API(req));
30 | }
31 |
32 | SQLProxyServer.prototype.execCommand = function(req){
33 | return this.loadAPI(req)
34 | .then(api => api.execute());
35 | }
36 | SQLProxyServer.prototype.getConnection = function(req){
37 | var consrc = [/\??con\=(.*)/.exec(req.url||"con=")[1], req.query.con, req.body.url];
38 | req.body.url = consrc.filter(s => s)[0];
39 | }
40 | SQLProxyServer.prototype.runStandalone = function(){
41 | var express = require('express');
42 | var bodyParser = require('body-parser');
43 | var app = express();
44 |
45 | app.use((req, res, next)=>{
46 | res.setHeader('Access-Control-Allow-Origin', '*');
47 | res.setHeader('Access-Control-Allow-Headers', 'Origin, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Response-Time, X-PINGOTHER, X-CSRF-Token,Authorization');
48 | res.setHeader('Access-Control-Allow-Methods', '*');
49 | res.setHeader('Access-Control-Expose-Headers', 'X-Api-Version, X-Request-Id, X-Response-Time');
50 | res.setHeader('Access-Control-Max-Age', '1000');
51 | return next();
52 | });
53 |
54 | app.use(bodyParser.json({
55 | strict: false,
56 | type: '*/*'
57 | }));
58 | var oThis = this;
59 | app.get("/", function(req, res){
60 | if(!req.query.con){
61 | return res.status(500).send("Missing parameter `con` with connection details. "
62 | + "Example: con=mssql://dbuser:dbpwd@sqlserver/databasename");
63 | }
64 | oThis.getConnection(req);
65 | oThis.execCommand({url: req.query.con, type:'test'})
66 | .then(data => res.send(data))
67 | .catch(err =>
68 | res.status(500).send(err));
69 | });
70 | app.post("/*", function(req, res){
71 | oThis.getConnection(req);
72 | oThis.execCommand(req.body)
73 | .then(data => res.send(data))
74 | .catch(err =>
75 | res.status(500).send(err));
76 | });
77 |
78 | var port = process.argv[2] || 666;
79 | app.listen(port);
80 | console.log("Server is listening to port " + port);
81 | }
82 |
83 | SQLProxyServer.prototype.API = function(command){
84 | var _api = this;
85 | _api.cmd = command;
86 |
87 | _api.execute = function(){
88 | return (_api[_api.cmd.type] || _api.default)();
89 | }
90 |
91 | _api.default = function(){
92 | return q.reject("Invalid command " + _api.cmd.type);
93 | }
94 |
95 | _api.test = function(){
96 | return _api.cmd.provider.query("SELECT 1;")
97 | .then(r => {return{status:'sucess'}})
98 | .catch(_api.internals.error);
99 | };
100 |
101 | _api.query = function(){
102 | var queries =
103 | _api.cmd.body.targets.map(t => {
104 | return _api.cmd.provider.query(t.target, _.assignIn({}, _api.cmd.body.range, t))
105 | .then(r =>
106 | _api.internals.parse(_api.cmd, t, r).results
107 | )});
108 | return q.all(queries)
109 | .then(_.concat)
110 | .then(_.flatten);
111 | }
112 |
113 | _api.search = function(){
114 | /**
115 | * Example array response
116 | * ["upper_25","upper_50","upper_75","upper_90","upper_95"]
117 | * Example map response
118 | * [ { "text" :"upper_25", "value": 1}, { "text" :"upper_75", "value": 2} ]
119 | */
120 | _api.cmd.body.targets = [{
121 | target: _api.cmd.body.target,
122 | type: 'search'
123 | }];
124 | return _api.query();
125 | }
126 |
127 | _api.annotations = function(){
128 | _api.cmd.body.targets = [{
129 | target: _api.cmd.body.annotation.query,
130 | annotation: _api.cmd.body.annotation,
131 | type: 'annotations'
132 | }];
133 | return _api.query();
134 | }
135 |
136 | _api.internals = {
137 | try: (fnc)=>{
138 | try{
139 | return fnc();
140 | }catch(e){
141 | console.error(e);
142 | return e;
143 | }
144 | },
145 | parse: (reqData, target, results)=>{
146 | if(target.type == "timeseries")
147 | return _api.internals.try(() => _api.internals.parseTimeseries(target, results));
148 | if(target.type == "table")
149 | return _api.internals.try(() => _api.internals.parseTable(target, results));
150 | if(target.type == "annotations")
151 | return _api.internals.try(() => _api.internals.parseAnnotations(target, results));
152 | if(target.type == "search")
153 | return _api.internals.try(() => _api.internals.parseSearch(target, results));
154 | return "Unsupported response type: " + target.type;
155 | },
156 | parseSearch: (target, results) => {
157 | var mapped = Object.keys(results.columns).length > 1;
158 | if(mapped){
159 | var textColumn = _api.internals.getColumn('text', target, results);
160 | var valueColumn = _api.internals.getColumn('value', target, results);
161 | target.results = results.rows.map(r => ({ "text" :r[textColumn], "value": r[valueColumn]}));
162 | }else{
163 | var col = Object.keys(results.columns)[0];
164 | target.results = results.rows.map(r => r[col]);
165 | }
166 | return target;
167 | },
168 | parseAnnotations: (target, results) => {
169 | target.timestamp = _api.internals.getTimestamp(target, results);
170 | target.results = results.rows.map(r => _.assignIn(r, {
171 | annotation: target.annotation,
172 | time: _api.internals.utc(r[target.timestamp], target.annotation.utc).valueOf()
173 | }));
174 | return target;
175 | },
176 | utc: function(value, utc){
177 | if(utc == 'localtime' && (value && value.getTimezoneOffset)){
178 | var dateTime = moment(value);
179 | return dateTime.add(value.getTimezoneOffset(), 'm');
180 | }
181 | return value;
182 | },
183 | parseTimeseries: (target, results)=>{
184 | target.timestamp = _api.internals.getTimestamp(target, results);
185 | target.metric = _api.internals.getMetric(target, results);
186 | target.value = _api.internals.getMetricValue(target, results);
187 | target.results = _.map(
188 | _.groupBy(results.rows, target.metric),
189 | (v, k) => {
190 | return {
191 | target: k,
192 | datapoints: v.map(r => [r[target.value], _api.internals.utc(r[target.timestamp], target.utc).valueOf()])
193 | };
194 | });
195 | return target;
196 | },
197 | parseTable: (target, results)=>{
198 | var mapType = function(sqltype){
199 | if(_.filter(['int', 'byte', 'decimal', 'float', 'double', 'money', 'bit',
200 | 'numeric', 'real'],
201 | (sqlt) => sqltype.indexOf(sqlt) != -1).length != 0)
202 | return "number";
203 | if(_.filter(['date', 'time'],
204 | (sqlt) => sqltype.indexOf(sqlt) != -1).length != 0)
205 | return "time";
206 | return "string";
207 | }
208 | var columns = _.map(results.columns, (v,k) =>{
209 | return {text:k, type:mapType(v.type.name.toLowerCase()), sqltype: v.type.name};
210 | });
211 | var rows = _.map(results.rows, (r) => _.map(r, (v) => v));
212 | target.results = {columns: columns, rows: rows, type:'table'};
213 | return target;
214 | },
215 | getTimestamp: (target, results)=>{
216 | return _api.internals.getSpecialColumn(results, (k, type) =>
217 | k == target.timestampKey ? 1000:
218 | k.toLowerCase() == 'timestamp' ? 100:
219 | results.columns[k].type.name == 'DateTime' ? 1: 0)
220 | },
221 | getMetric: (target, results)=>{
222 | return _api.internals.getSpecialColumn(results, (k, type) =>
223 | k == target.metric ? 1000:
224 | ['metric', 'key'].indexOf(k.toLowerCase()) != -1 ? 100:
225 | type == 'text' || type.indexOf('char') != -1 ? 1: 0)
226 | },
227 | getMetricValue: (target, results)=>{
228 | return _api.internals.getSpecialColumn(results, (k, type) =>
229 | k == target.value ? 1000:
230 | k.toLowerCase() == 'value' ? 100:
231 | [target.timestamp, target.metric].indexOf(k.toLowerCase()) == -1 ? 1: 0)
232 | },
233 | getColumn: (column, target, results)=>{
234 | return _api.internals.getSpecialColumn(results, (k, type) =>
235 | k == target.metric ? 1000:
236 | [column].indexOf(k.toLowerCase()) != -1 ? 100:
237 | type == 'text' || type.indexOf('char') != -1 ? 1: 0)
238 | },
239 | getSpecialColumn: (results, score)=>{
240 | return _.orderBy(Object.keys(results.columns)
241 | .map(k => {
242 | var type = results.columns[k].type.name.toLowerCase();
243 | return { key: k, score: score(k, type) }
244 | }),
245 | ['score'],
246 | ['desc'])[0].key
247 | }
248 | }
249 | return this;
250 | }
251 |
252 | if (require.main === module) {
253 | new SQLProxyServer().runStandalone();
254 | } else {
255 | module.exports = SQLProxyServer;
256 | }
257 |
258 |
--------------------------------------------------------------------------------