├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── crate-logo.png ├── dist ├── README.md ├── config_ctrl.d.ts ├── config_ctrl.js ├── config_ctrl.js.map ├── config_ctrl.ts ├── datasource.d.ts ├── datasource.js ├── datasource.js.map ├── datasource.ts ├── img │ ├── crate-datasource-add-src.png │ ├── crate-datasource-error.png │ ├── crate-datasource-graph.png │ ├── crate-datasource-nonvalidation.png │ └── crate_logo.png ├── module.d.ts ├── module.js ├── module.js.map ├── module.ts ├── partials │ ├── config.html │ ├── query.editor.html │ └── query.options.html ├── plugin.json ├── query_builder.d.ts ├── query_builder.js ├── query_builder.js.map ├── query_builder.ts ├── query_ctrl.d.ts ├── query_ctrl.js ├── query_ctrl.js.map ├── query_ctrl.ts ├── query_def.d.ts ├── query_def.js ├── query_def.js.map ├── query_def.ts ├── response_handler.d.ts ├── response_handler.js ├── response_handler.js.map ├── response_handler.ts └── sdk │ ├── query_ctrl.d.ts │ ├── query_ctrl.js │ ├── query_ctrl.js.map │ ├── query_ctrl.ts │ ├── sdk.d.ts │ ├── sdk.js │ ├── sdk.js.map │ └── sdk.ts ├── headers ├── common.d.ts ├── es6-shim │ └── es6-shim.d.ts ├── mocha │ └── mocha.d.ts └── zone │ └── zone.d.ts ├── package.json ├── src ├── config_ctrl.ts ├── datasource.ts ├── img │ ├── crate-datasource-add-src.png │ ├── crate-datasource-error.png │ ├── crate-datasource-graph.png │ ├── crate-datasource-nonvalidation.png │ └── crate_logo.png ├── module.ts ├── partials │ ├── config.html │ ├── query.editor.html │ └── query.options.html ├── plugin.json ├── query_builder.ts ├── query_ctrl.ts ├── query_def.ts ├── response_handler.ts ├── sdk │ ├── query_ctrl.ts │ └── sdk.ts └── spec │ ├── datasource_specs.js │ ├── query_builder_specs.js │ ├── response_handler_specs.js │ └── test-main.js ├── tsconfig.json ├── tsd.json └── tslint.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't diff files in dist/ 2 | *.map binary 3 | dist/** binary 4 | -------------------------------------------------------------------------------- /.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 | # Builded dist 12 | # /dist 13 | 14 | # Test artifacts 15 | /dist/test 16 | /dist/spec 17 | 18 | docs/AWS_S3_BUCKET 19 | docs/GIT_BRANCH 20 | docs/VERSION 21 | docs/GITCOMMIT 22 | docs/changed-files 23 | docs/changed-files 24 | 25 | # locally required config files 26 | public/css/*.min.css 27 | 28 | # Editor junk 29 | *.sublime-workspace 30 | *.sublime-project 31 | *.swp 32 | .idea/ 33 | *.iml 34 | 35 | /data/* 36 | /bin/* 37 | 38 | conf/custom.ini 39 | fig.yml 40 | profile.cov 41 | grafana 42 | .notouch 43 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowImplicitTypeConversion": ["string"], 3 | "disallowKeywords": ["with"], 4 | "disallowMultipleLineBreaks": true, 5 | "disallowMixedSpacesAndTabs": true, 6 | "disallowTrailingWhitespace": true, 7 | "requireSpacesInFunctionExpression": { 8 | "beforeOpeningCurlyBrace": true 9 | }, 10 | "disallowSpacesInsideArrayBrackets": true, 11 | "disallowSpacesInsideParentheses": true, 12 | "validateIndentation": 2 13 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | 4 | "bitwise":false, 5 | "curly": true, 6 | "eqnull": true, 7 | "strict": true, 8 | "module": true, 9 | "devel": true, 10 | "eqeqeq": true, 11 | "forin": false, 12 | "immed": true, 13 | "supernew": true, 14 | "expr": true, 15 | "indent": 2, 16 | "latedef": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": true, 20 | "undef": true, 21 | "boss": true, 22 | "trailing": true, 23 | "laxbreak": true, 24 | "laxcomma": true, 25 | "sub": true, 26 | "unused": true, 27 | "maxdepth": 6, 28 | "maxlen": 140, 29 | "esnext": true, 30 | 31 | "globals": { 32 | "System": true, 33 | "Promise": true, 34 | "define": true, 35 | "require": true, 36 | "Chromath": false, 37 | "setImmediate": true, 38 | "expect": true, 39 | "it": true, 40 | "describe": true, 41 | "sinon": true, 42 | "module": true, 43 | "beforeEach": true, 44 | "inject": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [Unreleased] 5 | 6 | 7 | ## [0.5.1] - 2017-04-05 8 | ### Changed 9 | - Use date_trunk() when interval set to second, minute, etc 10 | - Override limit only for Raw agg queries. 11 | 12 | 13 | ## [0.5.0] - 2017-03-22 14 | ### Added 15 | - Checks schema and table (prevent queries to different source). 16 | 17 | 18 | ## [0.4.0] - 2017-03-19 19 | ### Added 20 | - 'Auto' (uses date_trunk()) and 'Auto (Grafana)' (uses floor()) time intervals. 21 | 22 | ### Fixed 23 | - 10K issue 24 | 25 | ### Changed 26 | - Use explicit aggregation by time interval based on floor() instead date_trunk() 27 | 28 | ## [0.3.0] - 2017-03-02 29 | ### Added 30 | - Table mode support 31 | - Ad-hoc filters support 32 | - $timeFilter variable support 33 | - Quote column names with capital letters [#28](https://github.com/raintank/crate-datasource/issues/28) 34 | - Support GROUP BY in raw queries, issue [#30](https://github.com/raintank/crate-datasource/issues/30) 35 | 36 | ### Fixed 37 | - Schema queries (changed in Crate 1.0) 38 | 39 | 40 | ## [0.2.0] - 2016-11-29 41 | ### Added 42 | - Special "Raw" aggregation type [#9](https://github.com/raintank/crate-datasource/issues/9) 43 | - Alias for each field in SELECT 44 | 45 | 46 | ## [0.1.0] - 2016-07-10 47 | - Initial release 48 | - Implementation by [raintank](http://raintank.io) 49 | - Documentation contributions from [Crate.io](https://crate.io) 50 | -------------------------------------------------------------------------------- /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'], 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: ['watch-ts'], 30 | options: {spawn: false} 31 | } 32 | }, 33 | 34 | typescript: { 35 | build: { 36 | src: ['dist/**/*.ts', "!src/spec/**/*", "!**/*.d.ts"], 37 | dest: 'dist/', 38 | options: { 39 | module: 'system', //or commonjs 40 | target: 'es3', //or es5 41 | rootDir: 'dist/', 42 | keepDirectoryHierarchy: false, 43 | declaration: true, 44 | emitDecoratorMetadata: true, 45 | experimentalDecorators: true, 46 | sourceMap: true, 47 | noImplicitAny: false, 48 | } 49 | }, 50 | distTests: { 51 | src: ['src/**/*.ts', "!src/spec/**/*", "!**/*.d.ts"], 52 | dest: 'dist/test/', 53 | options: { 54 | module: 'commonjs', //or commonjs 55 | target: 'es5', //or es5 56 | rootDir: 'src/', 57 | sourceRoot: 'src/', 58 | declaration: true, 59 | emitDecoratorMetadata: true, 60 | experimentalDecorators: true, 61 | sourceMap: true, 62 | noImplicitAny: false, 63 | } 64 | }, 65 | // distTestsSpecs: { 66 | // src: ['src/spec/**/*.ts'], 67 | // dest: 'dist/test/', 68 | // options: { 69 | // module: 'commonjs', //or commonjs 70 | // target: 'es5', //or es5 71 | // declaration: true, 72 | // emitDecoratorMetadata: true, 73 | // experimentalDecorators: true, 74 | // sourceMap: true, 75 | // noImplicitAny: false, 76 | // } 77 | // } 78 | }, 79 | 80 | babel: { 81 | options: { 82 | sourceMap: true, 83 | presets: ['es2015'] 84 | }, 85 | distTestsSpecsNoSystemJs: { 86 | files: [{ 87 | expand: true, 88 | cwd: 'src/spec', 89 | src: ['**/*.js'], 90 | dest: 'dist/test/spec', 91 | ext:'.js' 92 | }] 93 | } 94 | }, 95 | 96 | mochaTest: { 97 | test: { 98 | options: { 99 | reporter: 'spec' 100 | }, 101 | src: ['dist/test/spec/test-main.js', 'dist/test/spec/*_specs.js'] 102 | } 103 | } 104 | }); 105 | 106 | grunt.registerTask('default', [ 107 | 'clean', 108 | 'copy', 109 | 'typescript:build', 110 | 'typescript:distTests', 111 | 'babel', 112 | 'mochaTest' 113 | ]); 114 | 115 | grunt.registerTask('watch-ts', [ 116 | 'clean', 117 | 'copy:src_to_dist', 118 | 'copy:pluginDef', 119 | 'typescript:build' 120 | ]); 121 | }; 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana Data Source Plugin for CrateDB 2 | 3 | [![Crate.io logo](https://raw.githubusercontent.com/raintank/crate-datasource/master/crate-logo.png)](https://crate.io) 4 | 5 | 6 | ## What Is CrateDB? 7 | CrateDB is a SQL database that makes it simple to store and analyze 8 | massive amounts of machine data in real-time. CrateDB customers have 9 | reported improving predictive analytic query performance of machine 10 | data by 20x more than MySQL, while reducing database hardware costs by 11 | 75%. 12 | 13 | Here’s how CrateDB makes this possible: 14 | 15 | - **Combining SQL & Search** into a single DBMS - allowing you to process any data structure...time series, geospatial, JSON, full-text, etc. 16 | - **Distributed query innovations** - that deliver real-time SQL performance 17 | - **An auto-scaling architecture** - grow CrateDB with less DBA expertise 18 | - **Dynamic schemas, adhoc queries** - quickly adapt to data structure changes 19 | 20 | For these reasons and more, CrateDB is your perfect datasource for Grafana. 21 | 22 | ## The CrateDB Datasource Plugin for Grafana 23 | 24 | ### Features 25 | Enables CrateDB clusters to act as data sources for your Grafana deployment, providing real-time analytical and time-series data with SQL. 26 | 27 | ### Requirements 28 | - **Grafana** > 3.x.x 29 | - **CrateDB** - All stable versions are supported by this plugin 30 | 31 | ### Setup 32 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-add-src.png) 33 | 34 | > The screenshot shows a connection to http://localhost:44200 which is a test database for the purpose of this tutorial. CrateDB's default binding is to http://localhost:4200. 35 | 36 | 1. Click on the Grafana icon on the top left. 37 | 2. After the menu opened, you should see a link `Data Sources` below `Dashboards`. 38 | 3. Click `+ Add data source`. 39 | 4. Select `CrateDB` from the 'Type' dropdown. 40 | 41 | #### Cross-origin Resource Sharing (CORS) 42 | 43 | CrateDB supports [cross-origin resource sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) and if Grafana is running on a different origin (e.g. another domain), it is required to configure CrateDB accordingly. For example by this is the minimum required configuration in your `crate.yml`: 44 | ``` 45 | http.cors.enabled: true 46 | http.cors.allow-origin: "http://mydomain.com" 47 | ``` 48 | > Replace http://mydomain.com with the domain Grafana is running on, or use a "*" if it's OK to allow any domain to access CrateDB 49 | 50 | For further options look in [CrateDB's documentation](https://crate.io/docs/reference/en/latest/configuration.html#cross-origin-resource-sharing-cors) 51 | 52 | #### The CrateDB Data Source 53 | 54 | Name | Description 55 | ------------ | ------------- 56 | Name | The data source name. 57 | Default | Set this data source as default for new panels. 58 | 59 | ##### HTTP Settings 60 | 61 | Name | Description 62 | ------------ | ------------- 63 | Url | The URI to any node in your CrateDB cluster. 64 | Access | Via Grafana backend (proxy) or directly from the browser (direct). 65 | Basic Auth | Enable basic authentication (only available via NGINX proxy in CrateDB). 66 | User | Not available in CrateDB. 67 | Password | Not available in CrateDB. 68 | 69 | ##### CrateDB Details 70 | 71 | These are specific settings for the CrateDB data source and it's required to set a fixed `schema`, `table`, and time series column per data source. 72 | 73 | Name | Description 74 | ------------ | ------------- 75 | Schema | CrateDB schema to query from (defaults to `doc`). 76 | Table | Table to retrieve the data from. Has to be available in the previously defined schema. 77 | Time Column | Time series column, has to be of type `timestamp` in CrateDB. 78 | Default grouping interval | The grouping resolution (can be changed by query). 79 | 80 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-nonvalidation.png) 81 | 82 | > Grafana will not check (yet) if the `time column`, the `schema`, or the `table` exists. Be sure to double check these values to avoid running into problems later. 83 | 84 | ### Querying CrateDB 85 | 86 | After adding a new dashboard and having the query editor open, define and run the queries you like - it's just like other SQL databases. For example we have added the [NYC yellow cab data set](http://www.nyc.gov/html/tlc/html/about/trip_record_data.shtml) in our cluster to show you something interesting! 87 | 88 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-graph.png) 89 | 90 | > This graph shows the number of yellow cab pick ups between on a weekend in August 2013. 91 | 92 | ### Debugging Queries 93 | 94 | Grafana runs queries almost immediately after change and it will also auto-complete columns or previous values. However, sometimes queries might still be invalid and Grafana will then show a small exclamation mark in the top corner of the graph. Clicking on it will give you the error message. 95 | 96 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-error.png) 97 | 98 | The CrateDB data source for Grafana supports a great range of scalar functions and operators. To read more about them, install or scale a cluster, or even to contribute to Crate, please have a look at the [official Crate documentation](https://crate.io/docs) 99 | 100 | ### License 101 | - This plugins is made available under the terms of the [Apache License, Version 2.0](https://github.com/crate/crate-datasource/blob/master/LICENSE). 102 | 103 | ## Getting Help 104 | 105 | - Read the CrateDB documentation [here](https://crate.io/docs) 106 | - Issues with the Grafana plugin can be reported or discussed [here](https://github.com/raintank/crate-datasource/issues) 107 | - Issues with CrateDB can be reported or discussed [here](https://github.com/crate/crate/issues) 108 | - Join the CrateDB Community Slack channel [here](https://crate.io/docs/support/slackin/) 109 | -------------------------------------------------------------------------------- /crate-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/crate-logo.png -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # Grafana Data Source Plugin for CrateDB 2 | 3 | [![Crate.io logo](https://raw.githubusercontent.com/raintank/crate-datasource/master/crate-logo.png)](https://crate.io) 4 | 5 | 6 | ## What Is CrateDB? 7 | CrateDB is a SQL database that makes it simple to store and analyze 8 | massive amounts of machine data in real-time. CrateDB customers have 9 | reported improving predictive analytic query performance of machine 10 | data by 20x more than MySQL, while reducing database hardware costs by 11 | 75%. 12 | 13 | Here’s how CrateDB makes this possible: 14 | 15 | - **Combining SQL & Search** into a single DBMS - allowing you to process any data structure...time series, geospatial, JSON, full-text, etc. 16 | - **Distributed query innovations** - that deliver real-time SQL performance 17 | - **An auto-scaling architecture** - grow CrateDB with less DBA expertise 18 | - **Dynamic schemas, adhoc queries** - quickly adapt to data structure changes 19 | 20 | For these reasons and more, CrateDB is your perfect datasource for Grafana. 21 | 22 | ## The CrateDB Datasource Plugin for Grafana 23 | 24 | ### Features 25 | Enables CrateDB clusters to act as data sources for your Grafana deployment, providing real-time analytical and time-series data with SQL. 26 | 27 | ### Requirements 28 | - **Grafana** > 3.x.x 29 | - **CrateDB** - All stable versions are supported by this plugin 30 | 31 | ### Setup 32 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-add-src.png) 33 | 34 | > The screenshot shows a connection to http://localhost:44200 which is a test database for the purpose of this tutorial. CrateDB's default binding is to http://localhost:4200. 35 | 36 | 1. Click on the Grafana icon on the top left. 37 | 2. After the menu opened, you should see a link `Data Sources` below `Dashboards`. 38 | 3. Click `+ Add data source`. 39 | 4. Select `CrateDB` from the 'Type' dropdown. 40 | 41 | #### Cross-origin Resource Sharing (CORS) 42 | 43 | CrateDB supports [cross-origin resource sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) and if Grafana is running on a different origin (e.g. another domain), it is required to configure CrateDB accordingly. For example by this is the minimum required configuration in your `crate.yml`: 44 | ``` 45 | http.cors.enabled: true 46 | http.cors.allow-origin: "http://mydomain.com" 47 | ``` 48 | > Replace http://mydomain.com with the domain Grafana is running on, or use a "*" if it's OK to allow any domain to access CrateDB 49 | 50 | For further options look in [CrateDB's documentation](https://crate.io/docs/reference/en/latest/configuration.html#cross-origin-resource-sharing-cors) 51 | 52 | #### The CrateDB Data Source 53 | 54 | Name | Description 55 | ------------ | ------------- 56 | Name | The data source name. 57 | Default | Set this data source as default for new panels. 58 | 59 | ##### HTTP Settings 60 | 61 | Name | Description 62 | ------------ | ------------- 63 | Url | The URI to any node in your CrateDB cluster. 64 | Access | Via Grafana backend (proxy) or directly from the browser (direct). 65 | Basic Auth | Enable basic authentication (only available via NGINX proxy in CrateDB). 66 | User | Not available in CrateDB. 67 | Password | Not available in CrateDB. 68 | 69 | ##### CrateDB Details 70 | 71 | These are specific settings for the CrateDB data source and it's required to set a fixed `schema`, `table`, and time series column per data source. 72 | 73 | Name | Description 74 | ------------ | ------------- 75 | Schema | CrateDB schema to query from (defaults to `doc`). 76 | Table | Table to retrieve the data from. Has to be available in the previously defined schema. 77 | Time Column | Time series column, has to be of type `timestamp` in CrateDB. 78 | Default grouping interval | The grouping resolution (can be changed by query). 79 | 80 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-nonvalidation.png) 81 | 82 | > Grafana will not check (yet) if the `time column`, the `schema`, or the `table` exists. Be sure to double check these values to avoid running into problems later. 83 | 84 | ### Querying CrateDB 85 | 86 | After adding a new dashboard and having the query editor open, define and run the queries you like - it's just like other SQL databases. For example we have added the [NYC yellow cab data set](http://www.nyc.gov/html/tlc/html/about/trip_record_data.shtml) in our cluster to show you something interesting! 87 | 88 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-graph.png) 89 | 90 | > This graph shows the number of yellow cab pick ups between on a weekend in August 2013. 91 | 92 | ### Debugging Queries 93 | 94 | Grafana runs queries almost immediately after change and it will also auto-complete columns or previous values. However, sometimes queries might still be invalid and Grafana will then show a small exclamation mark in the top corner of the graph. Clicking on it will give you the error message. 95 | 96 | ![](https://raw.githubusercontent.com/raintank/crate-datasource/master/src/img/crate-datasource-error.png) 97 | 98 | The CrateDB data source for Grafana supports a great range of scalar functions and operators. To read more about them, install or scale a cluster, or even to contribute to Crate, please have a look at the [official Crate documentation](https://crate.io/docs) 99 | 100 | ### License 101 | - This plugins is made available under the terms of the [Apache License, Version 2.0](https://github.com/crate/crate-datasource/blob/master/LICENSE). 102 | 103 | ## Getting Help 104 | 105 | - Read the CrateDB documentation [here](https://crate.io/docs) 106 | - Issues with the Grafana plugin can be reported or discussed [here](https://github.com/raintank/crate-datasource/issues) 107 | - Issues with CrateDB can be reported or discussed [here](https://github.com/crate/crate/issues) 108 | - Join the CrateDB Community Slack channel [here](https://crate.io/docs/support/slackin/) 109 | -------------------------------------------------------------------------------- /dist/config_ctrl.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare class CrateConfigCtrl { 3 | static templateUrl: string; 4 | current: any; 5 | timeIntervals: any[]; 6 | constructor($scope: any); 7 | } 8 | -------------------------------------------------------------------------------- /dist/config_ctrl.js: -------------------------------------------------------------------------------- 1 | /// 2 | System.register([], function(exports_1) { 3 | var CrateConfigCtrl; 4 | return { 5 | setters:[], 6 | execute: function() { 7 | CrateConfigCtrl = (function () { 8 | function CrateConfigCtrl($scope) { 9 | this.timeIntervals = [ 10 | { name: 'Auto', value: 'auto' }, 11 | { name: 'Auto (Grafana)', value: 'auto_gf' }, 12 | { name: 'Second', value: 'second' }, 13 | { name: 'Minute', value: 'minute' }, 14 | { name: 'Hour', value: 'hour' }, 15 | { name: 'Day', value: 'day' }, 16 | { name: 'Week', value: 'week' }, 17 | { name: 'Month', value: 'month' }, 18 | { name: 'Quarter', value: 'quarter' }, 19 | { name: 'Year', value: 'year' } 20 | ]; 21 | this.current.jsonData.timeInterval = this.current.jsonData.timeInterval || this.timeIntervals[1].value; 22 | } 23 | CrateConfigCtrl.templateUrl = 'partials/config.html'; 24 | return CrateConfigCtrl; 25 | })(); 26 | exports_1("CrateConfigCtrl", CrateConfigCtrl); 27 | } 28 | } 29 | }); 30 | //# sourceMappingURL=config_ctrl.js.map -------------------------------------------------------------------------------- /dist/config_ctrl.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"config_ctrl.js","sourceRoot":"","sources":["config_ctrl.ts"],"names":["CrateConfigCtrl","CrateConfigCtrl.constructor"],"mappings":"AAAA,8CAA8C;;;;;;YAK9C;gBAiBEA,yBAAYA,MAAMA;oBAblBC,kBAAaA,GAAUA;wBACrBA,EAACA,IAAIA,EAAEA,MAAMA,EAAKA,KAAKA,EAAEA,MAAMA,EAACA;wBAChCA,EAACA,IAAIA,EAAEA,gBAAgBA,EAAKA,KAAKA,EAAEA,SAASA,EAACA;wBAC7CA,EAACA,IAAIA,EAAEA,QAAQA,EAAGA,KAAKA,EAAEA,QAAQA,EAACA;wBAClCA,EAACA,IAAIA,EAAEA,QAAQA,EAAGA,KAAKA,EAAEA,QAAQA,EAACA;wBAClCA,EAACA,IAAIA,EAAEA,MAAMA,EAAKA,KAAKA,EAAEA,MAAMA,EAACA;wBAChCA,EAACA,IAAIA,EAAEA,KAAKA,EAAMA,KAAKA,EAAEA,KAAKA,EAACA;wBAC/BA,EAACA,IAAIA,EAAEA,MAAMA,EAAKA,KAAKA,EAAEA,MAAMA,EAACA;wBAChCA,EAACA,IAAIA,EAAEA,OAAOA,EAAIA,KAAKA,EAAEA,OAAOA,EAACA;wBACjCA,EAACA,IAAIA,EAAEA,SAASA,EAAEA,KAAKA,EAAEA,SAASA,EAACA;wBACnCA,EAACA,IAAIA,EAAEA,MAAMA,EAAKA,KAAKA,EAAEA,MAAMA,EAACA;qBACjCA,CAACA;oBAGAA,IAAIA,CAACA,OAAOA,CAACA,QAAQA,CAACA,YAAYA,GAAGA,IAAIA,CAACA,OAAOA,CAACA,QAAQA,CAACA,YAAYA,IAAIA,IAAIA,CAACA,aAAaA,CAACA,CAACA,CAACA,CAACA,KAAKA,CAACA;gBACzGA,CAACA;gBAlBMD,2BAAWA,GAAGA,sBAAsBA,CAACA;gBAmB9CA,sBAACA;YAADA,CAACA,AApBD,IAoBC;YApBD,6CAoBC,CAAA"} -------------------------------------------------------------------------------- /dist/config_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | 6 | export class CrateConfigCtrl { 7 | static templateUrl = 'partials/config.html'; 8 | current: any; 9 | 10 | timeIntervals: any[] = [ 11 | {name: 'Auto', value: 'auto'}, 12 | {name: 'Auto (Grafana)', value: 'auto_gf'}, 13 | {name: 'Second', value: 'second'}, 14 | {name: 'Minute', value: 'minute'}, 15 | {name: 'Hour', value: 'hour'}, 16 | {name: 'Day', value: 'day'}, 17 | {name: 'Week', value: 'week'}, 18 | {name: 'Month', value: 'month'}, 19 | {name: 'Quarter', value: 'quarter'}, 20 | {name: 'Year', value: 'year'} 21 | ]; 22 | 23 | constructor($scope) { 24 | this.current.jsonData.timeInterval = this.current.jsonData.timeInterval || this.timeIntervals[1].value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/datasource.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { CrateQueryBuilder } from './query_builder'; 3 | export declare class CrateDatasource { 4 | private $q; 5 | private backendSrv; 6 | private templateSrv; 7 | private timeSrv; 8 | type: string; 9 | url: string; 10 | name: string; 11 | basicAuth: string; 12 | withCredentials: boolean; 13 | schema: string; 14 | table: string; 15 | defaultTimeColumn: string; 16 | defaultGroupInterval: string; 17 | checkQuerySource: boolean; 18 | queryBuilder: CrateQueryBuilder; 19 | CRATE_ROWS_LIMIT: number; 20 | constructor(instanceSettings: any, $q: any, backendSrv: any, templateSrv: any, timeSrv: any); 21 | query(options: any): any; 22 | /** 23 | * Required. 24 | * Checks datasource and returns Crate cluster name and version or 25 | * error details. 26 | */ 27 | testDatasource(): any; 28 | metricFindQuery(query: string): any; 29 | getTimeFilter(timeFrom: any, timeTo: any): string; 30 | getTagKeys(options: any): any; 31 | getTagValues(options: any): any; 32 | setScopedVars(scopedVars: any): any; 33 | /** 34 | * Sends SQL query to Crate and returns result. 35 | * @param {string} query SQL query string 36 | * @param {any[]} args Optional query arguments 37 | * @return 38 | */ 39 | _sql_query(query: string, args?: any[]): any; 40 | checkSQLSource(query: any): void; 41 | _request(method: string, url: string, data?: any): any; 42 | _get(url?: string): any; 43 | _post(url: string, data?: any): any; 44 | } 45 | export declare function convertToCrateInterval(grafanaInterval: any): any; 46 | -------------------------------------------------------------------------------- /dist/datasource.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"datasource.js","sourceRoot":"","sources":["datasource.ts"],"names":["formatCrateValue","wrapWithQuotes","convertToCrateInterval","crateToMsInterval","getMinCrateInterval","CrateDatasource","CrateDatasource.constructor","CrateDatasource.query","CrateDatasource.testDatasource","CrateDatasource.metricFindQuery","CrateDatasource.getTimeFilter","CrateDatasource.getTagKeys","CrateDatasource.getTagValues","CrateDatasource.setScopedVars","CrateDatasource._sql_query","CrateDatasource.checkSQLSource","CrateDatasource._request","CrateDatasource._get","CrateDatasource._post"],"mappings":"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AMA,CAACA,CAACA,CAACA,KAAKA,CAACA;wBACjBA,CAACA,CAACA,CAACA;wBAEHA,MAAMA,CAACA,mBAACA,CAACA,GAAGA,CAACA,OAAOA,EAAEA,UAAAA,CAACA;4BACrBA,MAAMA,CAACA,KAAIA,CAACA,UAAUA,CAACA,CAACA,CAACA,KAAKA,EAAEA,CAACA,QAAQA,EAAEA,MAAMA,CAACA,CAACA;iCAChDA,IAAIA,CAACA,UAAAA,MAAMA;gCACVA,EAAEA,CAACA,CAACA,CAACA,CAACA,MAAMA,CAACA,CAACA,CAACA;oCACbA,MAAMA,CAACA,6BAAcA,CAACA,CAACA,CAACA,MAAMA,EAAEA,MAAMA,CAACA,CAACA;gCAC1CA,CAACA;gCAACA,IAAIA,CAACA,CAACA;oCACNA,MAAMA,CAACA,6BAAcA,CAACA,MAAMA,EAAEA,MAAMA,CAACA,CAACA;gCACxCA,CAACA;4BACHA,CAACA,CAACA,CAACA;wBACPA,CAACA,CAACA,CAAAA;oBACJA,CAACA,CAACA,CAACA;oBACHA,MAAMA,CAACA,IAAIA,CAACA,EAAEA,CAACA,GAAGA,CAACA,mBAACA,CAACA,YAAYA,CAACA,OAAOA,EAAEA,CAACA,CAACA,CAACA,CAACA,IAAIA,CAACA,UAAAA,MAAMA;wBACxDA,MAAMA,CAACA;4BACLA,IAAIA,EAAEA,mBAACA,CAACA,OAAOA,CAACA,MAAMA,CAACA;yBACxBA,CAACA;oBACJA,CAACA,CAACA,CAACA;gBACLA,CAACA;gBAEDF;;;;mBAIGA;gBACHA,wCAAcA,GAAdA;oBACEG,MAAMA,CAACA,IAAIA,CAACA,IAAIA,EAAEA;yBACjBA,IAAIA,CAACA,UAAAA,QAAQA;wBACZA,EAAEA,CAACA,CAACA,QAAQA,CAACA,QAAQA,KAAKA,GAAGA,CAACA,CAACA,CAACA;4BAC9BA,MAAMA,CAACA;gCACLA,MAAMA,EAAEA,SAASA;gCACjBA,OAAOA,EAAEA,WAAWA,GAAGA,QAAQA,CAACA,YAAYA;oCAC1CA,aAAaA,GAAGA,QAAQA,CAACA,OAAOA,CAACA,MAAMA;gCACzCA,KAAKA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"} -------------------------------------------------------------------------------- /dist/datasource.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | import * as dateMath from 'app/core/utils/datemath'; 5 | import moment from 'moment'; 6 | import {CrateQueryBuilder, getEnabledAggs, getRawAggs, getNotRawAggs} from './query_builder'; 7 | import handleResponse from './response_handler'; 8 | 9 | export class CrateDatasource { 10 | type: string; 11 | url: string; 12 | name: string; 13 | basicAuth: string; 14 | withCredentials: boolean; 15 | schema: string; 16 | table: string; 17 | defaultTimeColumn: string; 18 | defaultGroupInterval: string; 19 | checkQuerySource: boolean; 20 | queryBuilder: CrateQueryBuilder; 21 | CRATE_ROWS_LIMIT: number; 22 | 23 | 24 | constructor(instanceSettings, 25 | private $q, 26 | private backendSrv, 27 | private templateSrv, 28 | private timeSrv) { 29 | 30 | this.type = instanceSettings.type; 31 | this.url = instanceSettings.url; 32 | this.name = instanceSettings.name; 33 | this.basicAuth = instanceSettings.basicAuth; 34 | this.withCredentials = instanceSettings.withCredentials; 35 | this.schema = instanceSettings.jsonData.schema; 36 | this.table = instanceSettings.jsonData.table; 37 | this.defaultTimeColumn = instanceSettings.jsonData.timeColumn; 38 | this.defaultGroupInterval = instanceSettings.jsonData.timeInterval; 39 | this.checkQuerySource = instanceSettings.jsonData.checkQuerySource; 40 | 41 | this.$q = $q; 42 | this.backendSrv = backendSrv; 43 | this.templateSrv = templateSrv; 44 | this.timeSrv = timeSrv; 45 | 46 | this.queryBuilder = new CrateQueryBuilder(this.schema, 47 | this.table, 48 | this.defaultTimeColumn, 49 | this.defaultGroupInterval, 50 | this.templateSrv); 51 | 52 | this.CRATE_ROWS_LIMIT = 10000; 53 | } 54 | 55 | // Called once per panel (graph) 56 | query(options) { 57 | let timeFrom = Math.ceil(dateMath.parse(options.range.from)); 58 | let timeTo = Math.ceil(dateMath.parse(options.range.to)); 59 | let timeFilter = this.getTimeFilter(timeFrom, timeTo); 60 | let scopedVars = this.setScopedVars(options.scopedVars); 61 | 62 | let queries = _.map(options.targets, target => { 63 | if (target.hide || (target.rawQuery && !target.query)) { return []; } 64 | 65 | let query: string; 66 | let rawAggQuery: string; 67 | let queryTarget, rawAggTarget; 68 | let getQuery: any; 69 | let getRawAggQuery: any; 70 | let getRawAggInterval: any; 71 | let adhocFilters = this.templateSrv.getAdhocFilters(this.name); 72 | 73 | if (target.rawQuery) { 74 | query = target.query; 75 | } else { 76 | let minInterval = Math.ceil((timeTo - timeFrom) / this.CRATE_ROWS_LIMIT); 77 | let maxLimit = timeTo - timeFrom; 78 | let interval; 79 | 80 | if (target.timeInterval === 'auto') { 81 | interval = getMinCrateInterval(options.intervalMs); 82 | } else if (target.timeInterval === 'auto_gf') { 83 | // Use intervalMs for panel, provided by Grafana 84 | interval = options.intervalMs; 85 | } else { 86 | interval = target.timeInterval; 87 | } 88 | 89 | // Split target into two queries (with aggs and raw data) 90 | query = this.queryBuilder.buildAggQuery(target, interval, adhocFilters); 91 | queryTarget = _.cloneDeep(target); 92 | queryTarget.metricAggs = getNotRawAggs(queryTarget.metricAggs); 93 | 94 | rawAggQuery = this.queryBuilder.buildRawAggQuery(target, 0, adhocFilters, maxLimit); 95 | rawAggQuery = this.templateSrv.replace(rawAggQuery, scopedVars, formatCrateValue); 96 | rawAggTarget = _.cloneDeep(target); 97 | rawAggTarget.metricAggs = getRawAggs(rawAggTarget.metricAggs); 98 | } 99 | 100 | query = this.templateSrv.replace(query, scopedVars, formatCrateValue); 101 | 102 | let queries = [ 103 | {query: query, target: queryTarget}, 104 | {query: rawAggQuery, target: rawAggTarget} 105 | ]; 106 | queries = _.filter(queries, q => { 107 | return q.query; 108 | }); 109 | 110 | return _.map(queries, q => { 111 | return this._sql_query(q.query, [timeFrom, timeTo]) 112 | .then(result => { 113 | if (q.target) { 114 | return handleResponse(q.target, result); 115 | } else { 116 | return handleResponse(target, result); 117 | } 118 | }); 119 | }) 120 | }); 121 | return this.$q.all(_.flattenDepth(queries, 2)).then(result => { 122 | return { 123 | data: _.flatten(result) 124 | }; 125 | }); 126 | } 127 | 128 | /** 129 | * Required. 130 | * Checks datasource and returns Crate cluster name and version or 131 | * error details. 132 | */ 133 | testDatasource() { 134 | return this._get() 135 | .then(response => { 136 | if (response.$$status === 200) { 137 | return { 138 | status: "success", 139 | message: "Cluster: " + response.cluster_name + 140 | ", version: " + response.version.number, 141 | title: "Success" 142 | }; 143 | } 144 | }) 145 | .catch(error => { 146 | let message = error.statusText ? error.statusText + ': ' : ''; 147 | if (error.data && error.data.error) { 148 | message += error.data.error; 149 | } else if (error.data) { 150 | message += error.data; 151 | } else { 152 | message = "Can't connect to Crate instance"; 153 | } 154 | return { 155 | status: "error", 156 | message: message, 157 | title: "Error" 158 | }; 159 | }); 160 | } 161 | 162 | metricFindQuery(query: string) { 163 | if (!query) { 164 | return this.$q.when([]); 165 | } 166 | 167 | query = this.templateSrv.replace(query, null, formatCrateValue); 168 | return this._sql_query(query).then(result => { 169 | return _.map(_.flatten(result.rows), row => { 170 | return { 171 | text: row.toString(), 172 | value: row 173 | }; 174 | }); 175 | }); 176 | } 177 | 178 | getTimeFilter(timeFrom, timeTo) { 179 | return this.defaultTimeColumn + " >= '" + timeFrom + "' and " + this.defaultTimeColumn + " <= '" + timeTo + "'"; 180 | } 181 | 182 | getTagKeys(options) { 183 | let query = this.queryBuilder.getColumnsQuery(); 184 | return this.metricFindQuery(query); 185 | } 186 | 187 | getTagValues(options) { 188 | let range = this.timeSrv.timeRange(); 189 | let query = this.queryBuilder.getValuesQuery(options.key, this.CRATE_ROWS_LIMIT, range); 190 | return this.metricFindQuery(query); 191 | } 192 | 193 | setScopedVars(scopedVars) { 194 | scopedVars.crate_schema = {text: this.schema, value: `"${this.schema}"`}; 195 | scopedVars.crate_table = {text: this.table, value: `"${this.table}"`}; 196 | 197 | let crate_source = `"${this.schema}"."${this.table}"`; 198 | scopedVars.crate_source = {text: crate_source, value: crate_source}; 199 | 200 | return scopedVars; 201 | } 202 | 203 | /** 204 | * Sends SQL query to Crate and returns result. 205 | * @param {string} query SQL query string 206 | * @param {any[]} args Optional query arguments 207 | * @return 208 | */ 209 | _sql_query(query: string, args: any[] = []) { 210 | let data = { 211 | "stmt": query, 212 | "args": args 213 | }; 214 | 215 | if (this.checkQuerySource) { 216 | // Checks schema and table and throw error if it different from configured in data source 217 | this.checkSQLSource(query); 218 | } 219 | 220 | return this._post('_sql', data); 221 | } 222 | 223 | checkSQLSource(query) { 224 | let source_pattern = /.*[Ff][Rr][Oo][Mm]\s"?([^\.\s\"]*)"?\.?"?([^\.\s\"]*)"?/; 225 | let match = query.match(source_pattern); 226 | let schema = match[1]; 227 | let table = match[2]; 228 | if (schema !== this.schema || table !== this.table) { 229 | throw { message: `Schema and table should be ${this.schema}.${this.table}` }; 230 | } 231 | } 232 | 233 | _request(method: string, url: string, data?: any) { 234 | let options = { 235 | url: this.url + "/" + url, 236 | method: method, 237 | data: data, 238 | headers: { 239 | "Content-Type": "application/json" 240 | } 241 | }; 242 | 243 | if (this.basicAuth || this.withCredentials) { 244 | options["withCredentials"] = true; 245 | } 246 | if (this.basicAuth) { 247 | options.headers["Authorization"] = this.basicAuth; 248 | } 249 | 250 | return this.backendSrv.datasourceRequest(options) 251 | .then(response => { 252 | response.data.$$status = response.status; 253 | response.data.$$config = response.config; 254 | return response.data; 255 | }); 256 | } 257 | 258 | _get(url = "") { 259 | return this._request('GET', url); 260 | } 261 | 262 | _post(url: string, data?: any) { 263 | return this._request('POST', url, data); 264 | } 265 | } 266 | 267 | // Special value formatter for Crate. 268 | function formatCrateValue(value) { 269 | if (typeof value === 'string') { 270 | return wrapWithQuotes(value); 271 | } else { 272 | return value.map(v => wrapWithQuotes(v)).join(', '); 273 | } 274 | } 275 | 276 | function wrapWithQuotes(value) { 277 | if (!isNaN(value) || 278 | value.indexOf("'") != -1 || 279 | value.indexOf('"') != -1) { 280 | return value; 281 | } else { 282 | return "'" + value + "'"; 283 | } 284 | } 285 | 286 | export function convertToCrateInterval(grafanaInterval) { 287 | let crateIntervals = [ 288 | {shorthand: 's', value: 'second'}, 289 | {shorthand: 'm', value: 'minute'}, 290 | {shorthand: 'h', value: 'hour'}, 291 | {shorthand: 'd', value: 'day'}, 292 | {shorthand: 'w', value: 'week'}, 293 | {shorthand: 'M', value: 'month'}, 294 | {shorthand: 'y', value: 'year'} 295 | ]; 296 | let intervalRegex = /([\d]*)([smhdwMy])/; 297 | let parsedInterval = intervalRegex.exec(grafanaInterval); 298 | let value = Number(parsedInterval[1]); 299 | let unit = parsedInterval[2]; 300 | let crateInterval = _.find(crateIntervals, {'shorthand': unit}); 301 | return crateInterval ? crateInterval.value : undefined; 302 | } 303 | 304 | function crateToMsInterval(crateInterval: string) { 305 | let intervals_s = { 306 | 'year': 60 * 60 * 24 * 30 * 12, 307 | 'quarter': 60 * 60 * 24 * 30 * 3, 308 | 'month': 60 * 60 * 24 * 30, 309 | 'week': 60 * 60 * 24 * 7, 310 | 'day': 60 * 60 * 24, 311 | 'hour': 60 * 60, 312 | 'minute': 60, 313 | 'second': 1 314 | }; 315 | 316 | if (intervals_s[crateInterval]) { 317 | return intervals_s[crateInterval] * 1000; // Return ms 318 | } else { 319 | return undefined; 320 | } 321 | } 322 | 323 | function getMinCrateInterval(ms) { 324 | let seconds = ms / 1000; 325 | if (seconds > 60 * 60 * 24 * 30 * 3) 326 | return 'year'; 327 | else if (seconds > 60 * 60 * 24 * 30) // TODO: check defenition of month interval 328 | return 'quarter'; 329 | else if (seconds > 60 * 60 * 24 * 7) 330 | return 'month'; 331 | else if (seconds > 60 * 60 * 24) 332 | return 'week'; 333 | else if (seconds > 60 * 60) 334 | return 'day'; 335 | else if (seconds > 60) 336 | return 'hour'; 337 | else if (seconds > 1) 338 | return 'second'; 339 | else 340 | return 'second'; 341 | } 342 | -------------------------------------------------------------------------------- /dist/img/crate-datasource-add-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/img/crate-datasource-add-src.png -------------------------------------------------------------------------------- /dist/img/crate-datasource-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/img/crate-datasource-error.png -------------------------------------------------------------------------------- /dist/img/crate-datasource-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/img/crate-datasource-graph.png -------------------------------------------------------------------------------- /dist/img/crate-datasource-nonvalidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/img/crate-datasource-nonvalidation.png -------------------------------------------------------------------------------- /dist/img/crate_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/img/crate_logo.png -------------------------------------------------------------------------------- /dist/module.d.ts: -------------------------------------------------------------------------------- 1 | import { CrateDatasource } from './datasource'; 2 | import { CrateDatasourceQueryCtrl } from './query_ctrl'; 3 | import { CrateConfigCtrl } from './config_ctrl'; 4 | declare class CrateQueryOptionsCtrl { 5 | static templateUrl: string; 6 | } 7 | export { CrateDatasource as Datasource, CrateDatasourceQueryCtrl as QueryCtrl, CrateConfigCtrl as ConfigCtrl, CrateQueryOptionsCtrl as QueryOptionsCtrl }; 8 | -------------------------------------------------------------------------------- /dist/module.js: -------------------------------------------------------------------------------- 1 | System.register(['./datasource', './query_ctrl', './config_ctrl'], function(exports_1) { 2 | var datasource_1, query_ctrl_1, config_ctrl_1; 3 | var CrateQueryOptionsCtrl; 4 | return { 5 | setters:[ 6 | function (datasource_1_1) { 7 | datasource_1 = datasource_1_1; 8 | }, 9 | function (query_ctrl_1_1) { 10 | query_ctrl_1 = query_ctrl_1_1; 11 | }, 12 | function (config_ctrl_1_1) { 13 | config_ctrl_1 = config_ctrl_1_1; 14 | }], 15 | execute: function() { 16 | CrateQueryOptionsCtrl = (function () { 17 | function CrateQueryOptionsCtrl() { 18 | } 19 | CrateQueryOptionsCtrl.templateUrl = 'partials/query.options.html'; 20 | return CrateQueryOptionsCtrl; 21 | })(); 22 | exports_1("Datasource", datasource_1.CrateDatasource); 23 | exports_1("QueryCtrl", query_ctrl_1.CrateDatasourceQueryCtrl); 24 | exports_1("ConfigCtrl", config_ctrl_1.CrateConfigCtrl); 25 | exports_1("QueryOptionsCtrl", CrateQueryOptionsCtrl); 26 | } 27 | } 28 | }); 29 | //# sourceMappingURL=module.js.map -------------------------------------------------------------------------------- /dist/module.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"module.js","sourceRoot":"","sources":["module.ts"],"names":["CrateQueryOptionsCtrl","CrateQueryOptionsCtrl.constructor"],"mappings":";;;;;;;;;;;;;;;YAIA;gBAAAA;gBAEAC,CAACA;gBADQD,iCAAWA,GAAGA,6BAA6BA,CAACA;gBACrDA,4BAACA;YAADA,CAACA,AAFD,IAEC;YAGoB,qDAAU;YACD,6DAAS;YAClB,sDAAU;YACJ,oDAAgB"} -------------------------------------------------------------------------------- /dist/module.ts: -------------------------------------------------------------------------------- 1 | import {CrateDatasource} from './datasource'; 2 | import {CrateDatasourceQueryCtrl} from './query_ctrl'; 3 | import {CrateConfigCtrl} from './config_ctrl'; 4 | 5 | class CrateQueryOptionsCtrl { 6 | static templateUrl = 'partials/query.options.html'; 7 | } 8 | 9 | export { 10 | CrateDatasource as Datasource, 11 | CrateDatasourceQueryCtrl as QueryCtrl, 12 | CrateConfigCtrl as ConfigCtrl, 13 | CrateQueryOptionsCtrl as QueryOptionsCtrl 14 | }; 15 | -------------------------------------------------------------------------------- /dist/partials/config.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |

Crate details

7 | 8 |
9 |
10 |
11 | Schema 12 | 15 | 16 |
17 | 18 |
19 | Table 20 | 23 | 24 |
25 |
26 | 27 | 31 | 32 | 33 |
34 | Time column 35 | 39 | 40 |
41 | 42 |
43 | Default grouping interval 44 | 48 |
49 | 50 |
51 | -------------------------------------------------------------------------------- /dist/partials/query.editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
43 | 44 |
45 | 54 |
55 | 56 |
57 | 62 | 63 | 68 | 69 |
70 | 71 |
72 | 73 | 77 | 78 |
79 | 80 | 89 | 90 |
91 | 96 | 101 |
102 |
103 | 104 |
105 |
106 | 109 |
110 |
111 | 114 | 115 |
116 |
117 | 118 | 122 | 123 |
124 |
125 |
126 |
127 |
128 | 129 |
130 |
131 | Group By time Interval 132 | 137 |
138 |
139 | 140 |
141 | 142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | -------------------------------------------------------------------------------- /dist/partials/query.options.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/dist/partials/query.options.html -------------------------------------------------------------------------------- /dist/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Crate", 3 | "id": "crate-datasource", 4 | "type": "datasource", 5 | 6 | "partials": { 7 | "config": "public/app/plugins/datasource/crate/partials/config.html" 8 | }, 9 | 10 | "metrics": true, 11 | "annotations": false, 12 | 13 | "info": { 14 | "description": "Crate SQL Database datasource", 15 | "author": { 16 | "name": "Crate.IO", 17 | "url": "https://crate.io" 18 | }, 19 | "logos": { 20 | "small": "img/crate_logo.png", 21 | "large": "img/crate_logo.png" 22 | }, 23 | "links": [ 24 | {"name": "GitHub", "url": "https://github.com/raintank/crate-datasource"}, 25 | {"name": "MIT License", "url": "https://github.com/raintank/crate-datasource/blob/master/LICENSE"} 26 | ], 27 | "version": "0.5.1", 28 | "updated": "2017-04-05" 29 | }, 30 | 31 | "dependencies": { 32 | "grafanaVersion": "3.x.x", 33 | "plugins": [] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dist/query_builder.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare class CrateQueryBuilder { 3 | private templateSrv; 4 | schema: string; 5 | table: string; 6 | defaultTimeColumn: string; 7 | defaultGroupInterval: string; 8 | constructor(schema: string, table: string, defaultTimeColumn: string, defaultGroupInterval: string, templateSrv: any); 9 | /** 10 | * Builds Crate SQL query from given target object. 11 | * @param {any} target Target object. 12 | * @param {number} groupInterval Interval for grouping values. 13 | * @param {string} defaultAgg Default aggregation for values. 14 | * @return {string} SQL query. 15 | */ 16 | build(target: any, groupInterval?: number, adhocFilters?: any[], limit?: number, defaultAgg?: string): string; 17 | buildAggQuery(target: any, groupInterval?: number, adhocFilters?: any[], limit?: number): string; 18 | buildRawAggQuery(target: any, groupInterval?: number, adhocFilters?: any[], limit?: number): string; 19 | renderAdhocFilters(filters: any): any; 20 | /** 21 | * Builds SQL query for getting available columns from table. 22 | * @return {string} SQL query. 23 | */ 24 | getColumnsQuery(): string; 25 | getNumericColumnsQuery(): string; 26 | /** 27 | * Builds SQL query for getting unique values for given column. 28 | * @param {string} column Column name 29 | * @param {number} limit Optional. Limit number returned values. 30 | */ 31 | getValuesQuery(column: string, limit?: number, timeRange?: any): string; 32 | private renderMetricAggs(metricAggs, withAlias?); 33 | private renderWhereClauses(whereClauses); 34 | private containsVariable(str); 35 | } 36 | export declare function getSchemas(): string; 37 | export declare function getTables(schema: any): string; 38 | export declare function getEnabledAggs(metricAggs: any): any; 39 | export declare function getRawAggs(metricAggs: any): any; 40 | export declare function getNotRawAggs(metricAggs: any): any; 41 | -------------------------------------------------------------------------------- /dist/query_builder.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | // Maximum LIMIT value 6 | let MAX_LIMIT = 100000; 7 | let DEFAULT_LIMIT = 10000; 8 | 9 | export class CrateQueryBuilder { 10 | schema: string; 11 | table: string; 12 | defaultTimeColumn: string; 13 | defaultGroupInterval: string; 14 | 15 | constructor(schema: string, 16 | table: string, 17 | defaultTimeColumn: string, 18 | defaultGroupInterval: string, 19 | private templateSrv) { 20 | this.schema = schema; 21 | this.table = table; 22 | this.defaultTimeColumn = defaultTimeColumn; 23 | this.defaultGroupInterval = defaultGroupInterval; 24 | this.templateSrv = templateSrv; 25 | } 26 | 27 | /** 28 | * Builds Crate SQL query from given target object. 29 | * @param {any} target Target object. 30 | * @param {number} groupInterval Interval for grouping values. 31 | * @param {string} defaultAgg Default aggregation for values. 32 | * @return {string} SQL query. 33 | */ 34 | build(target: any, groupInterval = 0, adhocFilters = [], limit = DEFAULT_LIMIT, defaultAgg='avg') { 35 | let query: string; 36 | let timeExp: string; 37 | 38 | let timeColumn = quoteColumn(this.defaultTimeColumn); 39 | let aggs = getEnabledAggs(target.metricAggs); 40 | let rawAggs = getRawAggs(aggs); 41 | 42 | if (!aggs.length) { return null; } 43 | 44 | if (groupInterval) { 45 | // Manually aggregate by time interval, ie "SELECT floor(ts/10)*10 as time ..." 46 | timeExp = `floor(${timeColumn}/${groupInterval})*${groupInterval}`; 47 | aggs = aggregateMetrics(aggs, 'avg'); 48 | } else { 49 | timeExp = timeColumn; 50 | } 51 | 52 | // SELECT 53 | let renderedAggs = this.renderMetricAggs(aggs); 54 | query = "SELECT " + timeExp + " as time, " + renderedAggs; 55 | 56 | // Add GROUP BY columns to SELECT statement. 57 | if (target.groupByColumns && target.groupByColumns.length) { 58 | query += ", " + target.groupByColumns.join(', '); 59 | } 60 | query += ` FROM "${this.schema}"."${this.table}"` + 61 | ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 62 | 63 | // WHERE 64 | if (target.whereClauses && target.whereClauses.length) { 65 | query += " AND " + this.renderWhereClauses(target.whereClauses); 66 | } 67 | 68 | // Add ad-hoc filters 69 | if (adhocFilters.length > 0) { 70 | query += " AND " + this.renderAdhocFilters(adhocFilters); 71 | } 72 | 73 | // GROUP BY 74 | query += " GROUP BY time"; 75 | if (!groupInterval && rawAggs.length) { 76 | query += ", " + this.renderMetricAggs(rawAggs, false); 77 | } 78 | 79 | if (target.groupByColumns && target.groupByColumns.length) { 80 | query += ", " + target.groupByColumns.join(', '); 81 | } 82 | 83 | // If GROUP BY specified, sort also by selected columns 84 | query += " ORDER BY time"; 85 | if (target.groupByColumns && target.groupByColumns.length) { 86 | query += ", " + target.groupByColumns.join(', '); 87 | } 88 | query += " ASC"; 89 | 90 | return query; 91 | } 92 | 93 | buildAggQuery(target: any, groupInterval = 0, adhocFilters = [], limit?: number) { 94 | let query: string; 95 | let timeExp: string; 96 | 97 | let timeColumn = quoteColumn(this.defaultTimeColumn); 98 | let aggs = getEnabledAggs(target.metricAggs); 99 | aggs = getNotRawAggs(aggs); 100 | 101 | if (!aggs.length) { return null; } 102 | 103 | if (!groupInterval) { 104 | groupInterval = 1; 105 | } 106 | 107 | if (typeof groupInterval === 'number') { 108 | // Manually aggregate by time interval, ie "SELECT floor(ts/10)*10 as time ..." 109 | timeExp = `floor(${timeColumn}/${groupInterval})*${groupInterval}`; 110 | } else { 111 | // Use built-in date_trunc() function 112 | timeExp = `date_trunc('${groupInterval}', ${timeColumn})`; 113 | } 114 | 115 | // SELECT 116 | let renderedAggs = this.renderMetricAggs(aggs); 117 | query = `SELECT ${timeExp} as time, ${renderedAggs}`; 118 | 119 | // Add GROUP BY columns to SELECT statement. 120 | if (target.groupByColumns && target.groupByColumns.length) { 121 | query += ", " + target.groupByColumns.join(', '); 122 | } 123 | 124 | // FROM 125 | query += ` FROM "${this.schema}"."${this.table}"`; 126 | 127 | // WHERE 128 | query += ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 129 | if (target.whereClauses && target.whereClauses.length) { 130 | query += " AND " + this.renderWhereClauses(target.whereClauses); 131 | } 132 | 133 | // Add ad-hoc filters 134 | if (adhocFilters.length > 0) { 135 | query += " AND " + this.renderAdhocFilters(adhocFilters); 136 | } 137 | 138 | // GROUP BY 139 | query += " GROUP BY time"; 140 | 141 | if (target.groupByColumns && target.groupByColumns.length) { 142 | query += ", " + target.groupByColumns.join(', '); 143 | } 144 | 145 | // If GROUP BY specified, sort also by selected columns 146 | query += " ORDER BY time"; 147 | if (target.groupByColumns && target.groupByColumns.length) { 148 | query += ", " + target.groupByColumns.join(', '); 149 | } 150 | query += " ASC"; 151 | 152 | if (limit && limit > DEFAULT_LIMIT) { 153 | limit = Math.min(limit, MAX_LIMIT); 154 | query += ` LIMIT ${limit}`; 155 | } 156 | 157 | return query; 158 | } 159 | 160 | buildRawAggQuery(target: any, groupInterval = 0, adhocFilters = [], limit?: number) { 161 | let query: string; 162 | let timeExp: string; 163 | 164 | let timeColumn = quoteColumn(this.defaultTimeColumn); 165 | let aggs = getEnabledAggs(target.metricAggs); 166 | let rawAggs = getRawAggs(aggs); 167 | 168 | if (!rawAggs.length) { return null; } 169 | 170 | // SELECT 171 | let renderedAggs = this.renderMetricAggs(rawAggs); 172 | query = "SELECT " + timeColumn + " as time, " + renderedAggs; 173 | 174 | // Add GROUP BY columns to SELECT statement. 175 | if (target.groupByColumns && target.groupByColumns.length) { 176 | query += ", " + target.groupByColumns.join(', '); 177 | } 178 | query += ` FROM "${this.schema}"."${this.table}"` + 179 | ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 180 | 181 | // WHERE 182 | if (target.whereClauses && target.whereClauses.length) { 183 | query += " AND " + this.renderWhereClauses(target.whereClauses); 184 | } 185 | 186 | // Add ad-hoc filters 187 | if (adhocFilters.length > 0) { 188 | query += " AND " + this.renderAdhocFilters(adhocFilters); 189 | } 190 | 191 | // GROUP BY 192 | query += " GROUP BY time"; 193 | query += ", " + this.renderMetricAggs(rawAggs, false); 194 | 195 | if (target.groupByColumns && target.groupByColumns.length) { 196 | query += ", " + target.groupByColumns.join(', '); 197 | } 198 | 199 | // If GROUP BY specified, sort also by selected columns 200 | query += " ORDER BY time"; 201 | if (target.groupByColumns && target.groupByColumns.length) { 202 | query += ", " + target.groupByColumns.join(', '); 203 | } 204 | query += " ASC"; 205 | 206 | if (limit) { 207 | limit = Math.min(limit, MAX_LIMIT); 208 | query += ` LIMIT ${limit}`; 209 | } 210 | 211 | return query; 212 | } 213 | 214 | renderAdhocFilters(filters) { 215 | let conditions = _.map(filters, (tag, index) => { 216 | let filter_str = ""; 217 | let condition = tag.condition || 'AND'; 218 | let key = quoteColumn(tag.key); 219 | let operator = tag.operator; 220 | let value = quoteValue(tag.value); 221 | 222 | if (index > 0) { 223 | filter_str = `${condition} `; 224 | } 225 | 226 | if (operator === '=~') { 227 | operator = '~'; 228 | } 229 | 230 | filter_str += `${key} ${operator} ${value}` 231 | return filter_str; 232 | }); 233 | return conditions.join(' '); 234 | } 235 | 236 | /** 237 | * Builds SQL query for getting available columns from table. 238 | * @return {string} SQL query. 239 | */ 240 | getColumnsQuery() { 241 | let query = "SELECT column_name " + 242 | "FROM information_schema.columns " + 243 | "WHERE table_schema = '" + this.schema + "' " + 244 | "AND table_name = '" + this.table + "' " + 245 | "ORDER BY 1"; 246 | return query; 247 | } 248 | 249 | getNumericColumnsQuery() { 250 | return "SELECT column_name " + 251 | "FROM information_schema.columns " + 252 | "WHERE table_schema = '" + this.schema + "' " + 253 | "AND table_name = '" + this.table + "' " + 254 | "AND data_type in ('integer', 'long', 'short', 'double', 'float', 'byte') " + 255 | "ORDER BY 1"; 256 | } 257 | 258 | /** 259 | * Builds SQL query for getting unique values for given column. 260 | * @param {string} column Column name 261 | * @param {number} limit Optional. Limit number returned values. 262 | */ 263 | getValuesQuery(column: string, limit?: number, timeRange?) { 264 | let timeColumn = quoteColumn(this.defaultTimeColumn); 265 | let query = `SELECT DISTINCT ${column} ` + 266 | `FROM "${this.schema}"."${this.table}"`; 267 | 268 | if (timeRange) { 269 | let timeFrom = timeRange.from.valueOf(); 270 | let timeTo = timeRange.to.valueOf(); 271 | query += ` WHERE ${timeColumn} >= ${timeFrom} AND ${timeColumn} <= ${timeTo}`; 272 | } 273 | 274 | if (limit) { 275 | query += ` LIMIT ${limit}`; 276 | } 277 | return query; 278 | } 279 | 280 | private renderMetricAggs(metricAggs: any, withAlias=true): string { 281 | let enabledAggs = _.filter(metricAggs, (agg) => { 282 | return !agg.hide; 283 | }); 284 | 285 | let renderedAggs = _.map(enabledAggs, (agg) => { 286 | let alias = ''; 287 | if (agg.alias && withAlias) { 288 | alias = ' AS \"' + agg.alias + '\"'; 289 | } 290 | 291 | let column = quoteColumn(agg.column); 292 | if (agg.type === 'count_distinct') { 293 | return "count(distinct " + column + ")" + alias; 294 | } else if (agg.type === 'raw') { 295 | return column + alias; 296 | } else { 297 | return agg.type + "(" + column + ")" + alias; 298 | } 299 | }); 300 | 301 | if (renderedAggs.length) { 302 | return renderedAggs.join(', '); 303 | } else { 304 | return ""; 305 | } 306 | } 307 | 308 | private renderWhereClauses(whereClauses): string { 309 | let renderedClauses = _.map(whereClauses, (clauseObj, index) => { 310 | let rendered = ""; 311 | if (index !== 0) { 312 | rendered += clauseObj.condition + " "; 313 | } 314 | 315 | // Quote arguments as required by the operator and value type 316 | let rendered_value: string; 317 | let value = clauseObj.value; 318 | if (clauseObj.operator.toLowerCase() === 'in') { 319 | // Handle IN operator. Split comma-separated values. 320 | // "42, 10, a" => 42, 10, 'a' 321 | rendered_value = '(' + _.map(value.split(','), v => { 322 | v = v.trim(); 323 | if (!isNaN(v) || this.containsVariable(v)) { 324 | return v; 325 | } else { 326 | return "'" + v + "'"; 327 | } 328 | }).join(', ') + ')'; 329 | } else { 330 | rendered_value = _.map(value.split(','), v => { 331 | v = v.trim(); 332 | if (!isNaN(v) || this.containsVariable(v)) { 333 | return v; 334 | } else { 335 | return "'" + v + "'"; 336 | } 337 | }).join(', '); 338 | } 339 | rendered += clauseObj.column + ' ' + clauseObj.operator + ' ' + rendered_value; 340 | return rendered; 341 | }); 342 | return renderedClauses.join(' '); 343 | } 344 | 345 | // Check for template variables 346 | private containsVariable(str: string): boolean { 347 | let variables = _.map(this.templateSrv.variables, 'name'); 348 | return _.some(variables, variable => { 349 | let pattern = new RegExp('\\$' + variable); 350 | return pattern.test(str); 351 | }); 352 | } 353 | } 354 | 355 | export function getSchemas() { 356 | var query = "SELECT schema_name " + 357 | "FROM information_schema.schemata " + 358 | "WHERE schema_name NOT IN ('information_schema', 'blob', 'sys', 'pg_catalog') " + 359 | "ORDER BY 1"; 360 | return query; 361 | } 362 | 363 | export function getTables(schema) { 364 | var query = "SELECT table_name " + 365 | "FROM information_schema.tables " + 366 | "WHERE table_schema='" + schema + "' " + 367 | "ORDER BY 1"; 368 | return query; 369 | } 370 | 371 | function quoteColumn(column: string): string { 372 | if (isWithUpperCase(column)) { 373 | return '\"' + column + '\"'; 374 | } else { 375 | return column; 376 | } 377 | } 378 | 379 | function quoteValue(value: string): string { 380 | value = value.trim(); 381 | let match = value.match(/^'?([^']*)'?$/); 382 | if (match[1]) { 383 | value = match[1]; 384 | } else { 385 | return value; 386 | } 387 | 388 | if (!isNaN(Number(value))) { 389 | return value; 390 | } else { 391 | return "'" + value + "'"; 392 | } 393 | } 394 | 395 | function isWithUpperCase(str: string): boolean { 396 | return str.toLowerCase() !== str; 397 | } 398 | 399 | function aggregateMetrics(metricAggs: any, aggType: string) { 400 | let aggs = _.cloneDeep(metricAggs); 401 | return _.map(aggs, agg => { 402 | if (agg.type === 'raw') { 403 | agg.type = aggType; 404 | return agg; 405 | } else { 406 | return agg; 407 | } 408 | }); 409 | } 410 | 411 | export function getEnabledAggs(metricAggs) { 412 | return _.filter(metricAggs, (agg) => { 413 | return !agg.hide; 414 | }); 415 | } 416 | 417 | export function getRawAggs(metricAggs) { 418 | return _.filter(metricAggs, {type: 'raw'}); 419 | } 420 | 421 | export function getNotRawAggs(metricAggs) { 422 | return _.filter(metricAggs, agg => { 423 | return agg.type !== 'raw'; 424 | }); 425 | } 426 | -------------------------------------------------------------------------------- /dist/query_ctrl.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { QueryCtrl } from './sdk/sdk'; 3 | import { CrateQueryBuilder } from './query_builder'; 4 | export declare class CrateDatasourceQueryCtrl extends QueryCtrl { 5 | private $q; 6 | private uiSegmentSrv; 7 | private templateSrv; 8 | static templateUrl: string; 9 | crateQueryBuilder: CrateQueryBuilder; 10 | groupBySegments: any; 11 | whereSegments: any; 12 | removeWhereSegment: any; 13 | operators: any; 14 | timeIntervals: any[]; 15 | resultFormats: any[]; 16 | constructor($scope: any, $injector: any, $q: any, uiSegmentSrv: any, templateSrv: any); 17 | crateQuery(query: any, args?: any[]): any; 18 | getCollapsedText(): string; 19 | onChangeInternal(): void; 20 | groupBySegmentChanged(segment: any, index: any): void; 21 | onGroupByAliasChange(index: any): void; 22 | onAggTypeChange(): void; 23 | addMetricAgg(): void; 24 | removeMetricAgg(index: any): void; 25 | toggleShowMetric(agg: any): void; 26 | toggleEditorMode(): void; 27 | getColumns(allValue?: boolean, onlyNumeric?: boolean): any; 28 | getGroupByColumns(): any; 29 | getValues(column: any, limit?: number): any; 30 | getColumnsOrValues(segment: any, index: any): any; 31 | getMetricAggTypes(): ({ 32 | text: string; 33 | value: string; 34 | allValue: boolean; 35 | } | { 36 | text: string; 37 | value: string; 38 | allValue: boolean; 39 | anyDataType: boolean; 40 | })[]; 41 | getMetricAggDef(aggType: any): any; 42 | whereSegmentUpdated(segment: any, index: any): void; 43 | buildWhereSegments(whereClauses: any): void; 44 | buildWhereClauses(): void; 45 | fixSegments(segments: any): void; 46 | transformToSegments(results: any, addTemplateVars?: boolean): any; 47 | updateGroupByAliases(): void; 48 | } 49 | -------------------------------------------------------------------------------- /dist/query_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | import {QueryCtrl} from './sdk/sdk'; 6 | import {CrateQueryBuilder} from './query_builder'; 7 | import queryDef from './query_def'; 8 | 9 | export class CrateDatasourceQueryCtrl extends QueryCtrl { 10 | static templateUrl = 'partials/query.editor.html'; 11 | 12 | crateQueryBuilder: CrateQueryBuilder; 13 | groupBySegments: any; 14 | whereSegments: any; 15 | removeWhereSegment: any; 16 | operators: any; 17 | timeIntervals: any[]; 18 | resultFormats: any[]; 19 | 20 | constructor($scope, $injector, private $q, private uiSegmentSrv, private templateSrv) { 21 | super($scope, $injector); 22 | 23 | this.uiSegmentSrv = uiSegmentSrv; 24 | this.templateSrv = templateSrv; 25 | 26 | let ds = this.datasource; 27 | this.crateQueryBuilder = new CrateQueryBuilder(ds.schema, 28 | ds.table, 29 | ds.defaultTimeColumn, 30 | ds.defaultGroupInterval, 31 | templateSrv); 32 | 33 | this.operators = ['<', '>', '<=', '>=', '=', '<>', '!=', 'in', 'like', '~', '!~']; 34 | 35 | this.timeIntervals = [ 36 | {name: 'Auto', value: 'auto'}, 37 | {name: 'Auto (Grafana)', value: 'auto_gf'}, 38 | {name: 'Second', value: 'second'}, 39 | {name: 'Minute', value: 'minute'}, 40 | {name: 'Hour', value: 'hour'}, 41 | {name: 'Day', value: 'day'}, 42 | {name: 'Week', value: 'week'}, 43 | {name: 'Month', value: 'month'}, 44 | {name: 'Quarter', value: 'quarter'}, 45 | {name: 'Year', value: 'year'} 46 | ]; 47 | 48 | this.resultFormats = [ 49 | {text: 'Time series', value: 'time_series'}, 50 | {text: 'Table', value: 'table'}, 51 | ]; 52 | 53 | var target_defaults = { 54 | metricAggs: [ 55 | {type: 'avg', column: 'value'} 56 | ], 57 | groupByColumns: [], 58 | groupByAliases: [], 59 | whereClauses: [], 60 | timeInterval: ds.defaultGroupInterval, 61 | resultFormat: 'time_series' 62 | }; 63 | _.defaults(this.target, target_defaults); 64 | 65 | this.updateGroupByAliases(); 66 | 67 | this.groupBySegments = _.map(this.target.groupByColumns, this.uiSegmentSrv.newSegment); 68 | 69 | // Build WHERE segments 70 | this.whereSegments = []; 71 | this.buildWhereSegments(this.target.whereClauses); 72 | 73 | this.removeWhereSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove --'}); 74 | this.fixSegments(this.groupBySegments); 75 | } 76 | 77 | crateQuery(query, args = []) { 78 | return this.datasource._sql_query(query, args).then(response => { 79 | return response.rows; 80 | }); 81 | } 82 | 83 | getCollapsedText(): string { 84 | if (this.target.rawQuery) { 85 | return this.target.query; 86 | } else { 87 | return this.crateQueryBuilder.build(this.target); 88 | } 89 | } 90 | 91 | //////////////////// 92 | // Event handlers // 93 | //////////////////// 94 | 95 | onChangeInternal(): void { 96 | this.panelCtrl.refresh(); // Asks the panel to refresh data. 97 | } 98 | 99 | groupBySegmentChanged(segment, index): void { 100 | if (segment.type === 'plus-button') { 101 | segment.type = undefined; 102 | } 103 | this.target.groupByColumns = _.map(_.filter(this.groupBySegments, segment => { 104 | return (segment.type !== 'plus-button' && 105 | segment.value !== this.removeWhereSegment.value); 106 | }), 'value'); 107 | this.groupBySegments = _.map(this.target.groupByColumns, this.uiSegmentSrv.newSegment); 108 | this.groupBySegments.push(this.uiSegmentSrv.newPlusButton()); 109 | 110 | if (segment.value === this.removeWhereSegment.value) { 111 | this.target.groupByAliases.splice(index, 1); 112 | } 113 | this.updateGroupByAliases(); 114 | 115 | this.onChangeInternal(); 116 | } 117 | 118 | onGroupByAliasChange(index) { 119 | this.updateGroupByAliases(); 120 | this.onChangeInternal(); 121 | } 122 | 123 | onAggTypeChange(): void { 124 | this.onChangeInternal(); 125 | } 126 | 127 | addMetricAgg(): void { 128 | this.target.metricAggs.push({ type: 'avg', column: 'value' }); 129 | this.onChangeInternal(); 130 | } 131 | 132 | removeMetricAgg(index): void { 133 | this.target.metricAggs.splice(index, 1); 134 | this.onChangeInternal(); 135 | } 136 | 137 | toggleShowMetric(agg): void { 138 | agg.hide = !agg.hide; 139 | this.onChangeInternal(); 140 | } 141 | 142 | toggleEditorMode(): void { 143 | this.target.rawQuery = !this.target.rawQuery; 144 | } 145 | 146 | /////////////////////// 147 | // Query suggestions // 148 | /////////////////////// 149 | 150 | getColumns(allValue?: boolean, onlyNumeric?: boolean) { 151 | let query; 152 | if (onlyNumeric) { 153 | query = this.crateQueryBuilder.getNumericColumnsQuery(); 154 | } else { 155 | query = this.crateQueryBuilder.getColumnsQuery(); 156 | } 157 | let self = this; 158 | return this.crateQuery(query).then(rows => { 159 | if (allValue) { 160 | rows.splice(0, 0, '*'); 161 | } 162 | return self.transformToSegments(_.flatten(rows), true); 163 | }); 164 | } 165 | 166 | getGroupByColumns() { 167 | return this.getColumns().then(columns => { 168 | columns.splice(0, 0, angular.copy(this.removeWhereSegment)); 169 | return columns; 170 | }); 171 | } 172 | 173 | getValues(column, limit = 10) { 174 | let self = this; 175 | let time_range = this.panelCtrl.range; 176 | return this.crateQuery(this.crateQueryBuilder.getValuesQuery(column, limit, time_range)) 177 | .then(rows => { 178 | return self.transformToSegments(_.flatten(rows), true); 179 | }); 180 | } 181 | 182 | getColumnsOrValues(segment, index) { 183 | var self = this; 184 | if (segment.type === 'condition') { 185 | return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); 186 | } 187 | if (segment.type === 'operator') { 188 | return this.$q.when(this.uiSegmentSrv.newOperators(this.operators)); 189 | } 190 | 191 | if (segment.type === 'key' || segment.type === 'plus-button') { 192 | return this.getColumns().then(columns => { 193 | columns.splice(0, 0, angular.copy(this.removeWhereSegment)); 194 | return columns; 195 | }); 196 | } else if (segment.type === 'value') { 197 | return this.getValues(this.whereSegments[index - 2].value); 198 | } 199 | } 200 | 201 | getMetricAggTypes() { 202 | return queryDef.getMetricAggTypes(); 203 | } 204 | 205 | getMetricAggDef(aggType) { 206 | return _.find(this.getMetricAggTypes(), { value: aggType }); 207 | } 208 | 209 | whereSegmentUpdated(segment, index) { 210 | this.whereSegments[index] = segment; 211 | 212 | if (segment.value === this.removeWhereSegment.value) { 213 | this.whereSegments.splice(index, 3); 214 | if (this.whereSegments.length === 0) { 215 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 216 | } else if (this.whereSegments.length > 2) { 217 | this.whereSegments.splice(Math.max(index - 1, 0), 1); 218 | if (this.whereSegments[this.whereSegments.length - 1].type !== 'plus-button') { 219 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 220 | } 221 | } 222 | } else { 223 | if (segment.type === 'plus-button') { 224 | if (index > 2) { 225 | this.whereSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); 226 | } 227 | this.whereSegments.push(this.uiSegmentSrv.newOperator('=')); 228 | this.whereSegments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value')); 229 | segment.type = 'key'; 230 | segment.cssClass = 'query-segment-key'; 231 | } 232 | if ((index + 1) === this.whereSegments.length) { 233 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 234 | } 235 | } 236 | 237 | this.buildWhereClauses(); 238 | 239 | // Refresh only if all fields setted 240 | if (_.every(this.whereSegments, segment => { 241 | return ((segment.value || segment.type === 'plus-button') && 242 | !(segment.fake && segment.type !== 'plus-button')); 243 | })) { 244 | this.panelCtrl.refresh(); 245 | } 246 | } 247 | 248 | /////////////////////// 249 | 250 | buildWhereSegments(whereClauses: any): void { 251 | var self = this; 252 | _.forEach(whereClauses, whereClause => { 253 | if (whereClause.condition) { 254 | self.whereSegments.push(self.uiSegmentSrv.newCondition(whereClause.condition)); 255 | } 256 | self.whereSegments.push(self.uiSegmentSrv.newKey(whereClause.column)); 257 | self.whereSegments.push(self.uiSegmentSrv.newOperator(whereClause.operator)); 258 | self.whereSegments.push(self.uiSegmentSrv.newKeyValue(whereClause.value)); 259 | }); 260 | this.fixSegments(this.whereSegments); 261 | } 262 | 263 | buildWhereClauses() { 264 | var i = 0; 265 | var whereIndex = 0; 266 | var segments = this.whereSegments; 267 | var whereClauses = []; 268 | while (segments.length > i && segments[i].type !== 'plus-button') { 269 | if (whereClauses.length < whereIndex + 1) { 270 | whereClauses.push({condition: '', column: '', operator: '', value: ''}); 271 | } 272 | if (segments[i].type === 'condition') { 273 | whereClauses[whereIndex].condition = segments[i].value; 274 | } else if (segments[i].type === 'key') { 275 | whereClauses[whereIndex].column = segments[i].value; 276 | } else if (segments[i].type === 'operator') { 277 | whereClauses[whereIndex].operator = segments[i].value; 278 | } else if (segments[i].type === 'value') { 279 | whereClauses[whereIndex].value = segments[i].value; 280 | whereIndex++; 281 | } 282 | i++; 283 | } 284 | this.target.whereClauses = whereClauses; 285 | } 286 | 287 | fixSegments(segments) { 288 | var count = segments.length; 289 | var lastSegment = segments[Math.max(count-1, 0)]; 290 | 291 | if (!lastSegment || lastSegment.type !== 'plus-button') { 292 | segments.push(this.uiSegmentSrv.newPlusButton()); 293 | } 294 | } 295 | 296 | transformToSegments(results, addTemplateVars?: boolean) { 297 | var segments = _.map(_.flatten(results), value => { 298 | return this.uiSegmentSrv.newSegment({ 299 | value: value.toString(), 300 | expandable: false 301 | }); 302 | }); 303 | 304 | if (addTemplateVars) { 305 | for (let variable of this.templateSrv.variables) { 306 | segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true })); 307 | } 308 | } 309 | return segments; 310 | } 311 | 312 | updateGroupByAliases() { 313 | let groupByAliases = new Array(this.target.groupByColumns.length); 314 | this.target.groupByColumns.forEach((column, index) => { 315 | if (this.target.groupByAliases[index]) { 316 | groupByAliases[index] = this.target.groupByAliases[index]; 317 | } else { 318 | groupByAliases[index] = ""; 319 | } 320 | }); 321 | this.target.groupByAliases = groupByAliases; 322 | } 323 | 324 | } 325 | -------------------------------------------------------------------------------- /dist/query_def.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default class QueryDef { 3 | static getMetricAggTypes(): ({ 4 | text: string; 5 | value: string; 6 | allValue: boolean; 7 | } | { 8 | text: string; 9 | value: string; 10 | allValue: boolean; 11 | anyDataType: boolean; 12 | })[]; 13 | } 14 | -------------------------------------------------------------------------------- /dist/query_def.js: -------------------------------------------------------------------------------- 1 | /// 2 | System.register([], function(exports_1) { 3 | var _metricAggTypes, QueryDef; 4 | return { 5 | setters:[], 6 | execute: function() { 7 | _metricAggTypes = [ 8 | { text: "Raw", value: "raw", allValue: false }, 9 | { text: "Count", value: 'count', allValue: true, anyDataType: true }, 10 | { text: "Distinct Count", value: 'count_distinct', allValue: false, anyDataType: true }, 11 | { text: "Avg / Mean", value: 'avg', allValue: false }, 12 | { text: "Min", value: 'min', allValue: false }, 13 | { text: "Max", value: 'max', allValue: false }, 14 | { text: "Sum", value: 'sum', allValue: false }, 15 | { text: "Geometric Mean", value: 'geometric_mean', allValue: false }, 16 | { text: "Variance", value: 'variance', allValue: false }, 17 | { text: "Std Deviation", value: 'stddev', allValue: false }, 18 | { text: "Arbitrary", value: "arbitrary", allValue: false } 19 | ]; 20 | QueryDef = (function () { 21 | function QueryDef() { 22 | } 23 | QueryDef.getMetricAggTypes = function () { 24 | return _metricAggTypes; 25 | }; 26 | return QueryDef; 27 | })(); 28 | exports_1("default", QueryDef); 29 | } 30 | } 31 | }); 32 | //# sourceMappingURL=query_def.js.map -------------------------------------------------------------------------------- /dist/query_def.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query_def.js","sourceRoot":"","sources":["query_def.ts"],"names":["QueryDef","QueryDef.constructor","QueryDef.getMetricAggTypes"],"mappings":"AAAA,8CAA8C;;QAI1C,eAAe;;;;YAAf,eAAe,GAAG;gBACpB,EAAC,IAAI,EAAE,KAAK,EAAa,KAAK,EAAE,KAAK,EAAa,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,OAAO,EAAW,KAAK,EAAE,OAAO,EAAW,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAC;gBACpF,EAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAC;gBACrF,EAAC,IAAI,EAAE,YAAY,EAAM,KAAK,EAAE,KAAK,EAAa,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,KAAK,EAAa,KAAK,EAAE,KAAK,EAAa,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,KAAK,EAAa,KAAK,EAAE,KAAK,EAAa,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,KAAK,EAAa,KAAK,EAAE,KAAK,EAAa,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,UAAU,EAAQ,KAAK,EAAE,UAAU,EAAQ,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,eAAe,EAAG,KAAK,EAAE,QAAQ,EAAU,QAAQ,EAAE,KAAK,EAAC;gBAClE,EAAC,IAAI,EAAE,WAAW,EAAO,KAAK,EAAE,WAAW,EAAO,QAAQ,EAAE,KAAK,EAAC;aACnE,CAAC;YAEF;gBAAAA;gBAKAC,CAACA;gBAHQD,0BAAiBA,GAAxBA;oBACEE,MAAMA,CAACA,eAAeA,CAACA;gBACzBA,CAACA;gBACHF,eAACA;YAADA,CAACA,AALD,IAKC;YALD,8BAKC,CAAA"} -------------------------------------------------------------------------------- /dist/query_def.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | let _metricAggTypes = [ 6 | {text: "Raw", value: "raw", allValue: false}, 7 | {text: "Count", value: 'count', allValue: true, anyDataType: true}, 8 | {text: "Distinct Count", value: 'count_distinct', allValue: false, anyDataType: true}, 9 | {text: "Avg / Mean", value: 'avg', allValue: false}, 10 | {text: "Min", value: 'min', allValue: false}, 11 | {text: "Max", value: 'max', allValue: false}, 12 | {text: "Sum", value: 'sum', allValue: false}, 13 | {text: "Geometric Mean", value: 'geometric_mean', allValue: false}, 14 | {text: "Variance", value: 'variance', allValue: false}, 15 | {text: "Std Deviation", value: 'stddev', allValue: false}, 16 | {text: "Arbitrary", value: "arbitrary", allValue: false} 17 | ]; 18 | 19 | export default class QueryDef { 20 | 21 | static getMetricAggTypes() { 22 | return _metricAggTypes; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dist/response_handler.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default function handleResponse(target: any, result: any): any; 3 | -------------------------------------------------------------------------------- /dist/response_handler.js: -------------------------------------------------------------------------------- 1 | /// 2 | System.register(['lodash'], function(exports_1) { 3 | var lodash_1; 4 | function handleResponse(target, result) { 5 | if (target.resultFormat === 'table') { 6 | return handleTableResponse(target, result); 7 | } 8 | if (target.rawQuery) { 9 | return handleRawResponse(target, result); 10 | } 11 | else { 12 | return handleBuildedResponse(target, result); 13 | } 14 | } 15 | exports_1("default", handleResponse); 16 | function handleTableResponse(target, result) { 17 | return { 18 | columns: lodash_1["default"].map(result.cols, function (col) { 19 | return { text: col }; 20 | }), 21 | rows: result.rows, 22 | type: 'table' 23 | }; 24 | } 25 | function handleRawResponse(target, result) { 26 | var columns = result.cols; 27 | var timeColumnIndex = 0; 28 | var valueColumnIndex = 1; 29 | if (columns.length > 2) { 30 | var groupedResponse = lodash_1["default"].groupBy(result.rows, function (row) { 31 | // Assume row structure is 32 | // [ts, value, ...group by columns] 33 | return row.slice(2).join(' '); 34 | }); 35 | return lodash_1["default"].map(groupedResponse, function (rows, key) { 36 | return { 37 | target: key + ': ' + columns[valueColumnIndex], 38 | datapoints: convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) 39 | }; 40 | }); 41 | } 42 | else { 43 | return [{ 44 | target: columns[valueColumnIndex], 45 | datapoints: convertToGrafanaPoints(result.rows, timeColumnIndex, valueColumnIndex) 46 | }]; 47 | } 48 | } 49 | function handleBuildedResponse(target, result) { 50 | var columns = result.cols; 51 | var timeColumnIndex = 0; 52 | var groupByColumnIndexes, selectColumnIndexes; 53 | if (target.groupByColumns.length) { 54 | groupByColumnIndexes = lodash_1["default"].map(target.groupByColumns, function (groupByCol) { 55 | return lodash_1["default"].indexOf(columns, groupByCol); 56 | }); 57 | } 58 | var enabledAggs = lodash_1["default"].filter(target.metricAggs, function (agg) { 59 | return !agg.hide; 60 | }); 61 | if (enabledAggs.length) { 62 | selectColumnIndexes = lodash_1["default"].map(enabledAggs, function (metricAgg, index) { 63 | return index + 1; 64 | }); 65 | } 66 | else { 67 | return []; 68 | } 69 | if (groupByColumnIndexes && groupByColumnIndexes.length && !lodash_1["default"].some(groupByColumnIndexes, -1)) { 70 | var groupedResponse = lodash_1["default"].groupBy(result.rows, function (row) { 71 | // Construct groupBy key from Group By columns, for example: 72 | // [metric, host] => 'metric host' 73 | return lodash_1["default"].map(groupByColumnIndexes, function (columnIndex) { 74 | return row[columnIndex]; 75 | }).join(' '); 76 | }); 77 | return lodash_1["default"].flatten(lodash_1["default"].map(groupedResponse, function (rows, key) { 78 | return lodash_1["default"].map(selectColumnIndexes, function (valueIndex) { 79 | var datapoints = convertToGrafanaPoints(rows, timeColumnIndex, valueIndex); 80 | // Build alias for Group By column values 81 | var group_by_alias; 82 | if (rows.length) { 83 | group_by_alias = lodash_1["default"].map(groupByColumnIndexes, function (columnIndex, i) { 84 | var first_row = rows[0]; 85 | if (target.groupByAliases && target.groupByAliases[i]) { 86 | var pattern = new RegExp(target.groupByAliases[i]); 87 | var match = pattern.exec(first_row[columnIndex]); 88 | if (match && match.length > 1) { 89 | return match[1]; 90 | } 91 | else if (match) { 92 | return match[0]; 93 | } 94 | else { 95 | return first_row[columnIndex]; 96 | } 97 | } 98 | else { 99 | return first_row[columnIndex]; 100 | } 101 | }).join(' '); 102 | } 103 | else { 104 | group_by_alias = key; 105 | } 106 | return { 107 | target: group_by_alias + ': ' + columns[valueIndex], 108 | datapoints: datapoints 109 | }; 110 | }); 111 | })); 112 | } 113 | else { 114 | return lodash_1["default"].map(selectColumnIndexes, function (valueIndex) { 115 | var datapoints = convertToGrafanaPoints(result.rows, timeColumnIndex, valueIndex); 116 | return { 117 | target: columns[valueIndex], 118 | datapoints: datapoints 119 | }; 120 | }); 121 | } 122 | } 123 | function convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) { 124 | return lodash_1["default"].map(rows, function (row) { 125 | var ts = Number(row[timeColumnIndex]); 126 | var val = row[valueColumnIndex]; 127 | val = val !== null ? Number(val) : null; 128 | return [val, ts]; 129 | }); 130 | } 131 | function makeColName(aggType, column) { 132 | if (aggType === 'count_distinct') { 133 | return 'count(DISTINCT ' + column + ')'; 134 | } 135 | else { 136 | return aggType + '(' + column + ')'; 137 | } 138 | } 139 | return { 140 | setters:[ 141 | function (lodash_1_1) { 142 | lodash_1 = lodash_1_1; 143 | }], 144 | execute: function() { 145 | } 146 | } 147 | }); 148 | //# sourceMappingURL=response_handler.js.map -------------------------------------------------------------------------------- /dist/response_handler.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"response_handler.js","sourceRoot":"","sources":["response_handler.ts"],"names":["handleResponse","handleTableResponse","handleRawResponse","handleBuildedResponse","convertToGrafanaPoints","makeColName"],"mappings":"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CAACA,cAAcA,CAACA,CAACA,CAACA,CAACA,CAACA,CAACA;gCACtDA,IAAIA,OAAOA,GAAGA,IAAIA,MAAMA,CAACA,MAAMA,CAACA,cAAcA,CAACA,CAACA,CAACA,CAACA,CAACA;gCACnDA,IAAIA,KAAKA,GAAGA,OAAOA,CAACA,IAAIA,CAACA,SAASA,CAACA,WAAWA,CAACA,CAACA,CAACA;gCACjDA,EAAEA,CAACA,CAACA,KAAKA,IAAIA,KAAKA,CAACA,MAAMA,GAAGA,CAACA,CAACA,CAACA,CAACA;oCAC9BA,MAAMA,CAACA,KAAKA,CAACA,CAACA,CAACA,CAACA;gCAClBA,CAACA;gCAACA,IAAIA,CAACA,EAAEA,CAACA,CAACA,KAAKA,CAACA,CAAAA,CAACA;oCAChBA,MAAMA,CAACA,KAAKA,CAACA,CAACA,CAACA,CAACA;gCAClBA,CAACA;gCAACA,IAAIA,CAACA,CAACA;oCACNA,MAAMA,CAACA,SAASA,CAACA,WAAWA,CAACA,CAACA;gCAChCA,CAACA;4BACHA,CAACA;4BAACA,IAAIA,CAACA,CAACA;gCACNA,MAAMA,CAACA,SAASA,CAACA,WAAWA,CAACA,CAACA;4BAChCA,CAACA;wBACHA,CAACA,CAACA,CAACA,IAAIA,CAACA,GAAGA,CAACA,CAACA;oBACfA,CAACA;oBAACA,IAAIA,CAACA,CAACA;wBACNA,cAAcA,GAAGA,GAAGA,CAACA;oBACvBA,CAACA;oBAEDA,MAAMA,CAACA;wBACLA,MAAMA,EAAEA,cAAcA,GAAGA,IAAIA,GAAGA,OAAOA,CAACA,UAAUA,CAACA;wBACnDA,UAAUA,EAAEA,UAAUA;qBACvBA,CAACA;gBACJA,CAACA,CAACA,CAACA;YACLA,CAACA,CAACA,CAACA,CAACA;QACNA,CAACA;QAACA,IAAIA,CAACA,CAACA;YACNA,MAAMA,CAACA,mBAACA,CAACA,GAAGA,CAACA,mBAAmBA,EAAEA,UAACA,UAAUA;gBAC3CA,IAAIA,UAAUA,GAAGA,sBAAsBA,CAACA,MAAMA,CAACA,IAAIA,EAAEA,eAAeA,EAAEA,UAAUA,CAACA,CAACA;gBAElFA,MAAMA,CAACA;oBACLA,MAAMA,EAAEA,OAAOA,CAACA,UAAUA,CAACA;oBAC3BA,UAAUA,EAAEA,UAAUA;iBACvBA,CAACA;YACJA,CAACA,CAACA,CAACA;QACLA,CAACA;IACHA,CAACA;IAED,gCAAgC,IAAI,EAAE,eAAe,EAAE,gBAAgB;QACrEC,MAAMA,CAACA,mBAACA,CAACA,GAAGA,CAACA,IAAIA,EAAEA,UAAAA,GAAGA;YACpBA,IAAIA,EAAEA,GAAGA,MAAMA,CAACA,GAAGA,CAACA,eAAeA,CAACA,CAACA,CAACA;YACtCA,IAAIA,GAAGA,GAAGA,GAAGA,CAACA,gBAAgBA,CAACA,CAACA;YAChCA,GAAGA,GAAGA,GAAGA,KAAKA,IAAIA,GAAGA,MAAMA,CAACA,GAAGA,CAACA,GAAGA,IAAIA,CAACA;YAExCA,MAAMA,CAACA,CAACA,GAAGA,EAAEA,EAAEA,CAACA,CAACA;QACnBA,CAACA,CAACA,CAACA;IACLA,CAACA;IAED,qBAAqB,OAAO,EAAE,MAAM;QAClCC,EAAEA,CAACA,CAACA,OAAOA,KAAKA,gBAAgBA,CAACA,CAACA,CAACA;YACjCA,MAAMA,CAACA,iBAAiBA,GAAGA,MAAMA,GAAGA,GAAGA,CAACA;QAC1CA,CAACA;QAACA,IAAIA,CAACA,CAACA;YACNA,MAAMA,CAACA,OAAOA,GAAGA,GAAGA,GAAGA,MAAMA,GAAGA,GAAGA,CAACA;QACtCA,CAACA;IACHA,CAACA"} -------------------------------------------------------------------------------- /dist/response_handler.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | export default function handleResponse(target, result) { 6 | if (target.resultFormat === 'table') { 7 | return handleTableResponse(target, result); 8 | } 9 | 10 | if (target.rawQuery) { 11 | return handleRawResponse(target, result); 12 | } else { 13 | return handleBuildedResponse(target, result); 14 | } 15 | } 16 | 17 | function handleTableResponse(target, result) { 18 | return { 19 | columns: _.map(result.cols, col => { 20 | return {text: col}; 21 | }), 22 | rows: result.rows, 23 | type: 'table' 24 | }; 25 | } 26 | 27 | function handleRawResponse(target, result) { 28 | let columns = result.cols; 29 | let timeColumnIndex = 0; 30 | let valueColumnIndex = 1; 31 | 32 | if (columns.length > 2) { 33 | let groupedResponse = _.groupBy(result.rows, row => { 34 | // Assume row structure is 35 | // [ts, value, ...group by columns] 36 | return row.slice(2).join(' '); 37 | }); 38 | 39 | return _.map(groupedResponse, (rows, key) => { 40 | return { 41 | target: key + ': ' + columns[valueColumnIndex], 42 | datapoints: convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) 43 | }; 44 | }); 45 | } else { 46 | return [{ 47 | target: columns[valueColumnIndex], 48 | datapoints: convertToGrafanaPoints(result.rows, timeColumnIndex, valueColumnIndex) 49 | }]; 50 | } 51 | } 52 | 53 | function handleBuildedResponse(target, result) { 54 | let columns = result.cols; 55 | let timeColumnIndex = 0; 56 | let groupByColumnIndexes: number[], selectColumnIndexes: number[]; 57 | 58 | if (target.groupByColumns.length) { 59 | groupByColumnIndexes = _.map(target.groupByColumns, groupByCol => { 60 | return _.indexOf(columns, groupByCol); 61 | }); 62 | } 63 | 64 | let enabledAggs = _.filter(target.metricAggs, (agg) => { 65 | return !agg.hide; 66 | }); 67 | 68 | if (enabledAggs.length) { 69 | selectColumnIndexes = _.map(enabledAggs, (metricAgg, index) => { 70 | return index + 1; 71 | }); 72 | } else { 73 | return []; 74 | } 75 | 76 | if (groupByColumnIndexes && groupByColumnIndexes.length && !_.some(groupByColumnIndexes, -1)) { 77 | let groupedResponse = _.groupBy(result.rows, row => { 78 | // Construct groupBy key from Group By columns, for example: 79 | // [metric, host] => 'metric host' 80 | return _.map(groupByColumnIndexes, columnIndex => { 81 | return row[columnIndex]; 82 | }).join(' '); 83 | }); 84 | 85 | return _.flatten(_.map(groupedResponse, (rows, key) => { 86 | return _.map(selectColumnIndexes, (valueIndex) => { 87 | let datapoints = convertToGrafanaPoints(rows, timeColumnIndex, valueIndex); 88 | 89 | // Build alias for Group By column values 90 | let group_by_alias: string; 91 | if (rows.length) { 92 | group_by_alias = _.map(groupByColumnIndexes, (columnIndex, i) => { 93 | let first_row = rows[0]; 94 | if (target.groupByAliases && target.groupByAliases[i]) { 95 | let pattern = new RegExp(target.groupByAliases[i]); 96 | let match = pattern.exec(first_row[columnIndex]); 97 | if (match && match.length > 1) { 98 | return match[1]; 99 | } else if (match){ 100 | return match[0]; 101 | } else { 102 | return first_row[columnIndex]; 103 | } 104 | } else { 105 | return first_row[columnIndex]; 106 | } 107 | }).join(' '); 108 | } else { 109 | group_by_alias = key; 110 | } 111 | 112 | return { 113 | target: group_by_alias + ': ' + columns[valueIndex], 114 | datapoints: datapoints 115 | }; 116 | }); 117 | })); 118 | } else { 119 | return _.map(selectColumnIndexes, (valueIndex) => { 120 | let datapoints = convertToGrafanaPoints(result.rows, timeColumnIndex, valueIndex); 121 | 122 | return { 123 | target: columns[valueIndex], 124 | datapoints: datapoints 125 | }; 126 | }); 127 | } 128 | } 129 | 130 | function convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) { 131 | return _.map(rows, row => { 132 | let ts = Number(row[timeColumnIndex]); 133 | let val = row[valueColumnIndex]; 134 | val = val !== null ? Number(val) : null; 135 | 136 | return [val, ts]; 137 | }); 138 | } 139 | 140 | function makeColName(aggType, column) { 141 | if (aggType === 'count_distinct') { 142 | return 'count(DISTINCT ' + column + ')'; 143 | } else { 144 | return aggType + '(' + column + ')'; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /dist/sdk/query_ctrl.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare class QueryCtrl { 3 | $scope: any; 4 | private $injector; 5 | target: any; 6 | datasource: any; 7 | panelCtrl: any; 8 | panel: any; 9 | hasRawMode: boolean; 10 | error: string; 11 | constructor($scope: any, $injector: any); 12 | refresh(): void; 13 | } 14 | -------------------------------------------------------------------------------- /dist/sdk/query_ctrl.js: -------------------------------------------------------------------------------- 1 | /// 2 | System.register([], function(exports_1) { 3 | var QueryCtrl; 4 | return { 5 | setters:[], 6 | execute: function() { 7 | QueryCtrl = (function () { 8 | function QueryCtrl($scope, $injector) { 9 | this.$scope = $scope; 10 | this.$injector = $injector; 11 | this.panel = this.panelCtrl.panel; 12 | } 13 | QueryCtrl.prototype.refresh = function () { 14 | this.panelCtrl.refresh(); 15 | }; 16 | return QueryCtrl; 17 | })(); 18 | exports_1("QueryCtrl", QueryCtrl); 19 | } 20 | } 21 | }); 22 | //# sourceMappingURL=query_ctrl.js.map -------------------------------------------------------------------------------- /dist/sdk/query_ctrl.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query_ctrl.js","sourceRoot":"","sources":["query_ctrl.ts"],"names":["QueryCtrl","QueryCtrl.constructor","QueryCtrl.refresh"],"mappings":"AAAA,iDAAiD;;;;;;YAKjD;gBAQEA,mBAAmBA,MAAMA,EAAUA,SAASA;oBAAzBC,WAAMA,GAANA,MAAMA,CAAAA;oBAAUA,cAASA,GAATA,SAASA,CAAAA;oBAC1CA,IAAIA,CAACA,KAAKA,GAAGA,IAAIA,CAACA,SAASA,CAACA,KAAKA,CAACA;gBACpCA,CAACA;gBAEDD,2BAAOA,GAAPA;oBACEE,IAAIA,CAACA,SAASA,CAACA,OAAOA,EAAEA,CAACA;gBAC3BA,CAACA;gBAEHF,gBAACA;YAADA,CAACA,AAhBD,IAgBC;YAhBD,iCAgBC,CAAA"} -------------------------------------------------------------------------------- /dist/sdk/query_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | 6 | export class QueryCtrl { 7 | target: any; 8 | datasource: any; 9 | panelCtrl: any; 10 | panel: any; 11 | hasRawMode: boolean; 12 | error: string; 13 | 14 | constructor(public $scope, private $injector) { 15 | this.panel = this.panelCtrl.panel; 16 | } 17 | 18 | refresh() { 19 | this.panelCtrl.refresh(); 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /dist/sdk/sdk.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryCtrl } from './query_ctrl'; 2 | export declare function loadPluginCss(options: any): void; 3 | export { QueryCtrl }; 4 | -------------------------------------------------------------------------------- /dist/sdk/sdk.js: -------------------------------------------------------------------------------- 1 | System.register(['./query_ctrl', 'app/core/config'], function(exports_1) { 2 | var query_ctrl_1, config_1; 3 | function loadPluginCss(options) { 4 | if (config_1["default"].bootData.user.lightTheme) { 5 | System.import(options.light + '!css'); 6 | } 7 | else { 8 | System.import(options.dark + '!css'); 9 | } 10 | } 11 | exports_1("loadPluginCss", loadPluginCss); 12 | return { 13 | setters:[ 14 | function (query_ctrl_1_1) { 15 | query_ctrl_1 = query_ctrl_1_1; 16 | }, 17 | function (config_1_1) { 18 | config_1 = config_1_1; 19 | }], 20 | execute: function() { 21 | exports_1("QueryCtrl", query_ctrl_1.QueryCtrl); 22 | } 23 | } 24 | }); 25 | //# sourceMappingURL=sdk.js.map -------------------------------------------------------------------------------- /dist/sdk/sdk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"sdk.js","sourceRoot":"","sources":["sdk.ts"],"names":["loadPluginCss"],"mappings":";;IAIA,uBAA8B,OAAO;QACnCA,EAAEA,CAACA,CAACA,mBAAMA,CAACA,QAAQA,CAACA,IAAIA,CAACA,UAAUA,CAACA,CAACA,CAACA;YACpCA,MAAMA,CAACA,MAAMA,CAACA,OAAOA,CAACA,KAAKA,GAAGA,MAAMA,CAACA,CAACA;QACxCA,CAACA;QAACA,IAAIA,CAACA,CAACA;YACNA,MAAMA,CAACA,MAAMA,CAACA,OAAOA,CAACA,IAAIA,GAAGA,MAAMA,CAACA,CAACA;QACvCA,CAACA;IACHA,CAACA;IAND,yCAMC,CAAA;;;;;;;;;;YAGC,8CAAS"} -------------------------------------------------------------------------------- /dist/sdk/sdk.ts: -------------------------------------------------------------------------------- 1 | import {QueryCtrl} from './query_ctrl'; 2 | 3 | import config from 'app/core/config'; 4 | 5 | export function loadPluginCss(options) { 6 | if (config.bootData.user.lightTheme) { 7 | System.import(options.light + '!css'); 8 | } else { 9 | System.import(options.dark + '!css'); 10 | } 11 | } 12 | 13 | export { 14 | QueryCtrl 15 | } 16 | -------------------------------------------------------------------------------- /headers/common.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare var System: any; 4 | 5 | // dummy modules 6 | declare module 'app/core/config' { 7 | var config: any; 8 | export default config; 9 | } 10 | 11 | declare module 'lodash' { 12 | var lodash: any; 13 | export default lodash; 14 | } 15 | 16 | declare module 'moment' { 17 | var moment: any; 18 | export default moment; 19 | } 20 | 21 | declare module 'angular' { 22 | var angular: any; 23 | export default angular; 24 | } 25 | 26 | declare module 'jquery' { 27 | var jquery: any; 28 | export default jquery; 29 | } 30 | 31 | declare module 'app/core/utils/kbn' { 32 | var kbn: any; 33 | export default kbn; 34 | } 35 | 36 | // Hack for datemath module 37 | declare module 'app/core/utils/datemath' { 38 | export var parse: any; 39 | export var isValid: any; 40 | export var parseDateMath: any; 41 | } 42 | 43 | declare module 'app/core/store' { 44 | var store: any; 45 | export default store; 46 | } 47 | 48 | declare module 'tether' { 49 | var config: any; 50 | export default config; 51 | } 52 | 53 | declare module 'tether-drop' { 54 | var config: any; 55 | export default config; 56 | } 57 | 58 | declare module 'eventemitter3' { 59 | var config: any; 60 | export default config; 61 | } 62 | -------------------------------------------------------------------------------- /headers/mocha/mocha.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for mocha 2.0.1 2 | // Project: http://mochajs.org/ 3 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | interface Mocha { 7 | // Setup mocha with the given setting options. 8 | setup(options: MochaSetupOptions): Mocha; 9 | 10 | //Run tests and invoke `fn()` when complete. 11 | run(callback?: () => void): void; 12 | 13 | // Set reporter as function 14 | reporter(reporter: () => void): Mocha; 15 | 16 | // Set reporter, defaults to "dot" 17 | reporter(reporter: string): Mocha; 18 | 19 | // Enable growl support. 20 | growl(): Mocha 21 | } 22 | 23 | interface MochaSetupOptions { 24 | //milliseconds to wait before considering a test slow 25 | slow?: number; 26 | 27 | // timeout in milliseconds 28 | timeout?: number; 29 | 30 | // ui name "bdd", "tdd", "exports" etc 31 | ui?: string; 32 | 33 | //array of accepted globals 34 | globals?: any[]; 35 | 36 | // reporter instance (function or string), defaults to `mocha.reporters.Dot` 37 | reporter?: any; 38 | 39 | // bail on the first test failure 40 | bail?: boolean; 41 | 42 | // ignore global leaks 43 | ignoreLeaks?: boolean; 44 | 45 | // grep string or regexp to filter tests with 46 | grep?: any; 47 | } 48 | 49 | interface MochaDone { 50 | (error?: Error): void; 51 | } 52 | 53 | declare var mocha: Mocha; 54 | 55 | declare var describe : { 56 | (description: string, spec: () => void): void; 57 | only(description: string, spec: () => void): void; 58 | skip(description: string, spec: () => void): void; 59 | timeout(ms: number): void; 60 | } 61 | 62 | // alias for `describe` 63 | declare var context : { 64 | (contextTitle: string, spec: () => void): void; 65 | only(contextTitle: string, spec: () => void): void; 66 | skip(contextTitle: string, spec: () => void): void; 67 | timeout(ms: number): void; 68 | } 69 | 70 | declare var it: { 71 | (expectation: string, assertion?: () => void): void; 72 | (expectation: string, assertion?: (done: MochaDone) => void): void; 73 | only(expectation: string, assertion?: () => void): void; 74 | only(expectation: string, assertion?: (done: MochaDone) => void): void; 75 | skip(expectation: string, assertion?: () => void): void; 76 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 77 | timeout(ms: number): void; 78 | }; 79 | 80 | declare function before(action: () => void): void; 81 | 82 | declare function before(action: (done: MochaDone) => void): void; 83 | 84 | declare function setup(action: () => void): void; 85 | 86 | declare function setup(action: (done: MochaDone) => void): void; 87 | 88 | declare function after(action: () => void): void; 89 | 90 | declare function after(action: (done: MochaDone) => void): void; 91 | 92 | declare function teardown(action: () => void): void; 93 | 94 | declare function teardown(action: (done: MochaDone) => void): void; 95 | 96 | declare function beforeEach(action: () => void): void; 97 | 98 | declare function beforeEach(action: (done: MochaDone) => void): void; 99 | 100 | declare function suiteSetup(action: () => void): void; 101 | 102 | declare function suiteSetup(action: (done: MochaDone) => void): void; 103 | 104 | declare function afterEach(action: () => void): void; 105 | 106 | declare function afterEach(action: (done: MochaDone) => void): void; 107 | 108 | declare function suiteTeardown(action: () => void): void; 109 | 110 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 111 | 112 | declare module "mocha" { 113 | 114 | class Mocha { 115 | constructor(options?: { 116 | grep?: RegExp; 117 | ui?: string; 118 | reporter?: string; 119 | timeout?: number; 120 | bail?: boolean; 121 | }); 122 | 123 | bail(value?: boolean): Mocha; 124 | addFile(file: string): Mocha; 125 | reporter(value: string): Mocha; 126 | ui(value: string): Mocha; 127 | grep(value: string): Mocha; 128 | grep(value: RegExp): Mocha; 129 | invert(): Mocha; 130 | ignoreLeaks(value: boolean): Mocha; 131 | checkLeaks(): Mocha; 132 | growl(): Mocha; 133 | globals(value: string): Mocha; 134 | globals(values: string[]): Mocha; 135 | useColors(value: boolean): Mocha; 136 | useInlineDiffs(value: boolean): Mocha; 137 | timeout(value: number): Mocha; 138 | slow(value: number): Mocha; 139 | enableTimeouts(value: boolean): Mocha; 140 | asyncOnly(value: boolean): Mocha; 141 | noHighlighting(value: boolean): Mocha; 142 | 143 | run(onComplete?: (failures: number) => void): void; 144 | } 145 | 146 | export = Mocha; 147 | } 148 | -------------------------------------------------------------------------------- /headers/zone/zone.d.ts: -------------------------------------------------------------------------------- 1 | declare module Zone { 2 | export class Stacktrace { 3 | constructor(e: Error); 4 | get(): string; 5 | } 6 | } 7 | 8 | 9 | declare class Zone { 10 | constructor(parentZone: Zone, data: any); 11 | fork(locals: any): Zone; 12 | bind(fn, skipEnqueue): void; 13 | bindOnce(fn): any; 14 | run(fn, applyTo?, applyWith?): void; 15 | beforeTask(): void; 16 | onZoneCreated(): void; 17 | afterTask(): void; 18 | enqueueTask(): void; 19 | dequeueTask(): void; 20 | 21 | static patchSetClearFn(obj, fnNames): string; 22 | static patchPrototype(obj, fnNames): any; 23 | static bindArguments(args: any[]): any; 24 | static bindArgumentsOnce(args: any[]): any; 25 | static patchableFn(obj, fnNames): any 26 | static patchProperty(obj, prop): void; 27 | static patchProperties(obj, properties): void; 28 | static patchEventTargetMethods(obj): void; 29 | static patch(): void; 30 | static canPatchViaPropertyDescriptor(): boolean; 31 | static patchViaPropertyDescriptor(): void; 32 | static patchViaCapturingAllTheEvents(): void; 33 | static patchWebSocket(): void; 34 | static patchClass(className: string): void; 35 | static patchMutationObserverClass(className: string): void; 36 | static patchDefineProperty(): void; 37 | static patchRegisterElement(): void; 38 | static eventNames: string; 39 | static onEventNames: string; 40 | static init(): void; 41 | static exceptZone: { 42 | boringZone: Zone; 43 | interestingZone: Zone, 44 | beforeTask: () => void; 45 | afterTask: () => void; 46 | fork: (ops: any) => Zone; 47 | }; 48 | static longStackTraceZone: { 49 | getLongStacktrace(exception: any): string; 50 | stackFramesFilter(line: string): boolean; 51 | onError(exception): void; 52 | fork(locals): Zone; 53 | }; 54 | static getStacktrace(): Zone.Stacktrace; 55 | static countingZone: { 56 | '+enqueueTask': () => void; 57 | '-dequeueTask': () => void; 58 | '+afterTask': () => void; 59 | counter: () => void; 60 | data: { 61 | count: number; 62 | flushed: boolean; 63 | }; 64 | onFlush: () => void; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crate-datasource", 3 | "private": true, 4 | "version": "0.0.1", 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/raintank/crate-datasource.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/raintank/crate-datasource/issues" 18 | }, 19 | "devDependencies": { 20 | "babel": "~6.5.1", 21 | "chai": "~3.5.0", 22 | "grunt": "~0.4.5", 23 | "grunt-babel": "~6.0.0", 24 | "grunt-contrib-clean": "~0.6.0", 25 | "grunt-contrib-copy": "~0.8.2", 26 | "grunt-contrib-uglify": "~0.11.0", 27 | "grunt-contrib-watch": "^0.6.1", 28 | "grunt-execute": "~0.2.2", 29 | "grunt-mocha-test": "~0.12.7", 30 | "grunt-systemjs-builder": "^0.2.5", 31 | "grunt-tslint": "^3.0.2", 32 | "grunt-typescript": "^0.8.0", 33 | "jsdom": "~3.1.2", 34 | "load-grunt-tasks": "~3.2.0", 35 | "prunk": "~1.2.1", 36 | "q": "~1.4.1" 37 | }, 38 | "dependencies": { 39 | "babel-plugin-transform-es2015-for-of": "^6.6.0", 40 | "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", 41 | "babel-preset-es2015": "^6.5.0", 42 | "lodash": "~4.0.0", 43 | "mocha": "^2.4.5", 44 | "tslint": "^3.4.0", 45 | "typescript": "^1.7.5" 46 | }, 47 | "homepage": "https://github.com/raintank/crate-datasource#readme" 48 | } 49 | -------------------------------------------------------------------------------- /src/config_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | 6 | export class CrateConfigCtrl { 7 | static templateUrl = 'partials/config.html'; 8 | current: any; 9 | 10 | timeIntervals: any[] = [ 11 | {name: 'Auto', value: 'auto'}, 12 | {name: 'Auto (Grafana)', value: 'auto_gf'}, 13 | {name: 'Second', value: 'second'}, 14 | {name: 'Minute', value: 'minute'}, 15 | {name: 'Hour', value: 'hour'}, 16 | {name: 'Day', value: 'day'}, 17 | {name: 'Week', value: 'week'}, 18 | {name: 'Month', value: 'month'}, 19 | {name: 'Quarter', value: 'quarter'}, 20 | {name: 'Year', value: 'year'} 21 | ]; 22 | 23 | constructor($scope) { 24 | this.current.jsonData.timeInterval = this.current.jsonData.timeInterval || this.timeIntervals[1].value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/datasource.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | import * as dateMath from 'app/core/utils/datemath'; 5 | import moment from 'moment'; 6 | import {CrateQueryBuilder, getEnabledAggs, getRawAggs, getNotRawAggs} from './query_builder'; 7 | import handleResponse from './response_handler'; 8 | 9 | export class CrateDatasource { 10 | type: string; 11 | url: string; 12 | name: string; 13 | basicAuth: string; 14 | withCredentials: boolean; 15 | schema: string; 16 | table: string; 17 | defaultTimeColumn: string; 18 | defaultGroupInterval: string; 19 | checkQuerySource: boolean; 20 | queryBuilder: CrateQueryBuilder; 21 | CRATE_ROWS_LIMIT: number; 22 | 23 | 24 | constructor(instanceSettings, 25 | private $q, 26 | private backendSrv, 27 | private templateSrv, 28 | private timeSrv) { 29 | 30 | this.type = instanceSettings.type; 31 | this.url = instanceSettings.url; 32 | this.name = instanceSettings.name; 33 | this.basicAuth = instanceSettings.basicAuth; 34 | this.withCredentials = instanceSettings.withCredentials; 35 | this.schema = instanceSettings.jsonData.schema; 36 | this.table = instanceSettings.jsonData.table; 37 | this.defaultTimeColumn = instanceSettings.jsonData.timeColumn; 38 | this.defaultGroupInterval = instanceSettings.jsonData.timeInterval; 39 | this.checkQuerySource = instanceSettings.jsonData.checkQuerySource; 40 | 41 | this.$q = $q; 42 | this.backendSrv = backendSrv; 43 | this.templateSrv = templateSrv; 44 | this.timeSrv = timeSrv; 45 | 46 | this.queryBuilder = new CrateQueryBuilder(this.schema, 47 | this.table, 48 | this.defaultTimeColumn, 49 | this.defaultGroupInterval, 50 | this.templateSrv); 51 | 52 | this.CRATE_ROWS_LIMIT = 10000; 53 | } 54 | 55 | // Called once per panel (graph) 56 | query(options) { 57 | let timeFrom = Math.ceil(dateMath.parse(options.range.from)); 58 | let timeTo = Math.ceil(dateMath.parse(options.range.to)); 59 | let timeFilter = this.getTimeFilter(timeFrom, timeTo); 60 | let scopedVars = this.setScopedVars(options.scopedVars); 61 | 62 | let queries = _.map(options.targets, target => { 63 | if (target.hide || (target.rawQuery && !target.query)) { return []; } 64 | 65 | let query: string; 66 | let rawAggQuery: string; 67 | let queryTarget, rawAggTarget; 68 | let getQuery: any; 69 | let getRawAggQuery: any; 70 | let getRawAggInterval: any; 71 | let adhocFilters = this.templateSrv.getAdhocFilters(this.name); 72 | 73 | if (target.rawQuery) { 74 | query = target.query; 75 | } else { 76 | let minInterval = Math.ceil((timeTo - timeFrom) / this.CRATE_ROWS_LIMIT); 77 | let maxLimit = timeTo - timeFrom; 78 | let interval; 79 | 80 | if (target.timeInterval === 'auto') { 81 | interval = getMinCrateInterval(options.intervalMs); 82 | } else if (target.timeInterval === 'auto_gf') { 83 | // Use intervalMs for panel, provided by Grafana 84 | interval = options.intervalMs; 85 | } else { 86 | interval = target.timeInterval; 87 | } 88 | 89 | // Split target into two queries (with aggs and raw data) 90 | query = this.queryBuilder.buildAggQuery(target, interval, adhocFilters); 91 | queryTarget = _.cloneDeep(target); 92 | queryTarget.metricAggs = getNotRawAggs(queryTarget.metricAggs); 93 | 94 | rawAggQuery = this.queryBuilder.buildRawAggQuery(target, 0, adhocFilters, maxLimit); 95 | rawAggQuery = this.templateSrv.replace(rawAggQuery, scopedVars, formatCrateValue); 96 | rawAggTarget = _.cloneDeep(target); 97 | rawAggTarget.metricAggs = getRawAggs(rawAggTarget.metricAggs); 98 | } 99 | 100 | query = this.templateSrv.replace(query, scopedVars, formatCrateValue); 101 | 102 | let queries = [ 103 | {query: query, target: queryTarget}, 104 | {query: rawAggQuery, target: rawAggTarget} 105 | ]; 106 | queries = _.filter(queries, q => { 107 | return q.query; 108 | }); 109 | 110 | return _.map(queries, q => { 111 | return this._sql_query(q.query, [timeFrom, timeTo]) 112 | .then(result => { 113 | if (q.target) { 114 | return handleResponse(q.target, result); 115 | } else { 116 | return handleResponse(target, result); 117 | } 118 | }); 119 | }) 120 | }); 121 | return this.$q.all(_.flattenDepth(queries, 2)).then(result => { 122 | return { 123 | data: _.flatten(result) 124 | }; 125 | }); 126 | } 127 | 128 | /** 129 | * Required. 130 | * Checks datasource and returns Crate cluster name and version or 131 | * error details. 132 | */ 133 | testDatasource() { 134 | return this._get() 135 | .then(response => { 136 | if (response.$$status === 200) { 137 | return { 138 | status: "success", 139 | message: "Cluster: " + response.cluster_name + 140 | ", version: " + response.version.number, 141 | title: "Success" 142 | }; 143 | } 144 | }) 145 | .catch(error => { 146 | let message = error.statusText ? error.statusText + ': ' : ''; 147 | if (error.data && error.data.error) { 148 | message += error.data.error; 149 | } else if (error.data) { 150 | message += error.data; 151 | } else { 152 | message = "Can't connect to Crate instance"; 153 | } 154 | return { 155 | status: "error", 156 | message: message, 157 | title: "Error" 158 | }; 159 | }); 160 | } 161 | 162 | metricFindQuery(query: string) { 163 | if (!query) { 164 | return this.$q.when([]); 165 | } 166 | 167 | query = this.templateSrv.replace(query, null, formatCrateValue); 168 | return this._sql_query(query).then(result => { 169 | return _.map(_.flatten(result.rows), row => { 170 | return { 171 | text: row.toString(), 172 | value: row 173 | }; 174 | }); 175 | }); 176 | } 177 | 178 | getTimeFilter(timeFrom, timeTo) { 179 | return this.defaultTimeColumn + " >= '" + timeFrom + "' and " + this.defaultTimeColumn + " <= '" + timeTo + "'"; 180 | } 181 | 182 | getTagKeys(options) { 183 | let query = this.queryBuilder.getColumnsQuery(); 184 | return this.metricFindQuery(query); 185 | } 186 | 187 | getTagValues(options) { 188 | let range = this.timeSrv.timeRange(); 189 | let query = this.queryBuilder.getValuesQuery(options.key, this.CRATE_ROWS_LIMIT, range); 190 | return this.metricFindQuery(query); 191 | } 192 | 193 | setScopedVars(scopedVars) { 194 | scopedVars.crate_schema = {text: this.schema, value: `"${this.schema}"`}; 195 | scopedVars.crate_table = {text: this.table, value: `"${this.table}"`}; 196 | 197 | let crate_source = `"${this.schema}"."${this.table}"`; 198 | scopedVars.crate_source = {text: crate_source, value: crate_source}; 199 | 200 | return scopedVars; 201 | } 202 | 203 | /** 204 | * Sends SQL query to Crate and returns result. 205 | * @param {string} query SQL query string 206 | * @param {any[]} args Optional query arguments 207 | * @return 208 | */ 209 | _sql_query(query: string, args: any[] = []) { 210 | let data = { 211 | "stmt": query, 212 | "args": args 213 | }; 214 | 215 | if (this.checkQuerySource) { 216 | // Checks schema and table and throw error if it different from configured in data source 217 | this.checkSQLSource(query); 218 | } 219 | 220 | return this._post('_sql', data); 221 | } 222 | 223 | checkSQLSource(query) { 224 | let source_pattern = /.*[Ff][Rr][Oo][Mm]\s"?([^\.\s\"]*)"?\.?"?([^\.\s\"]*)"?/; 225 | let match = query.match(source_pattern); 226 | let schema = match[1]; 227 | let table = match[2]; 228 | if (schema !== this.schema || table !== this.table) { 229 | throw { message: `Schema and table should be ${this.schema}.${this.table}` }; 230 | } 231 | } 232 | 233 | _request(method: string, url: string, data?: any) { 234 | let options = { 235 | url: this.url + "/" + url, 236 | method: method, 237 | data: data, 238 | headers: { 239 | "Content-Type": "application/json" 240 | } 241 | }; 242 | 243 | if (this.basicAuth || this.withCredentials) { 244 | options["withCredentials"] = true; 245 | } 246 | if (this.basicAuth) { 247 | options.headers["Authorization"] = this.basicAuth; 248 | } 249 | 250 | return this.backendSrv.datasourceRequest(options) 251 | .then(response => { 252 | response.data.$$status = response.status; 253 | response.data.$$config = response.config; 254 | return response.data; 255 | }); 256 | } 257 | 258 | _get(url = "") { 259 | return this._request('GET', url); 260 | } 261 | 262 | _post(url: string, data?: any) { 263 | return this._request('POST', url, data); 264 | } 265 | } 266 | 267 | // Special value formatter for Crate. 268 | function formatCrateValue(value) { 269 | if (typeof value === 'string') { 270 | return wrapWithQuotes(value); 271 | } else { 272 | return value.map(v => wrapWithQuotes(v)).join(', '); 273 | } 274 | } 275 | 276 | function wrapWithQuotes(value) { 277 | if (!isNaN(value) || 278 | value.indexOf("'") != -1 || 279 | value.indexOf('"') != -1) { 280 | return value; 281 | } else { 282 | return "'" + value + "'"; 283 | } 284 | } 285 | 286 | export function convertToCrateInterval(grafanaInterval) { 287 | let crateIntervals = [ 288 | {shorthand: 's', value: 'second'}, 289 | {shorthand: 'm', value: 'minute'}, 290 | {shorthand: 'h', value: 'hour'}, 291 | {shorthand: 'd', value: 'day'}, 292 | {shorthand: 'w', value: 'week'}, 293 | {shorthand: 'M', value: 'month'}, 294 | {shorthand: 'y', value: 'year'} 295 | ]; 296 | let intervalRegex = /([\d]*)([smhdwMy])/; 297 | let parsedInterval = intervalRegex.exec(grafanaInterval); 298 | let value = Number(parsedInterval[1]); 299 | let unit = parsedInterval[2]; 300 | let crateInterval = _.find(crateIntervals, {'shorthand': unit}); 301 | return crateInterval ? crateInterval.value : undefined; 302 | } 303 | 304 | function crateToMsInterval(crateInterval: string) { 305 | let intervals_s = { 306 | 'year': 60 * 60 * 24 * 30 * 12, 307 | 'quarter': 60 * 60 * 24 * 30 * 3, 308 | 'month': 60 * 60 * 24 * 30, 309 | 'week': 60 * 60 * 24 * 7, 310 | 'day': 60 * 60 * 24, 311 | 'hour': 60 * 60, 312 | 'minute': 60, 313 | 'second': 1 314 | }; 315 | 316 | if (intervals_s[crateInterval]) { 317 | return intervals_s[crateInterval] * 1000; // Return ms 318 | } else { 319 | return undefined; 320 | } 321 | } 322 | 323 | function getMinCrateInterval(ms) { 324 | let seconds = ms / 1000; 325 | if (seconds > 60 * 60 * 24 * 30 * 3) 326 | return 'year'; 327 | else if (seconds > 60 * 60 * 24 * 30) // TODO: check defenition of month interval 328 | return 'quarter'; 329 | else if (seconds > 60 * 60 * 24 * 7) 330 | return 'month'; 331 | else if (seconds > 60 * 60 * 24) 332 | return 'week'; 333 | else if (seconds > 60 * 60) 334 | return 'day'; 335 | else if (seconds > 60) 336 | return 'hour'; 337 | else if (seconds > 1) 338 | return 'second'; 339 | else 340 | return 'second'; 341 | } 342 | -------------------------------------------------------------------------------- /src/img/crate-datasource-add-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/img/crate-datasource-add-src.png -------------------------------------------------------------------------------- /src/img/crate-datasource-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/img/crate-datasource-error.png -------------------------------------------------------------------------------- /src/img/crate-datasource-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/img/crate-datasource-graph.png -------------------------------------------------------------------------------- /src/img/crate-datasource-nonvalidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/img/crate-datasource-nonvalidation.png -------------------------------------------------------------------------------- /src/img/crate_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/img/crate_logo.png -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import {CrateDatasource} from './datasource'; 2 | import {CrateDatasourceQueryCtrl} from './query_ctrl'; 3 | import {CrateConfigCtrl} from './config_ctrl'; 4 | 5 | class CrateQueryOptionsCtrl { 6 | static templateUrl = 'partials/query.options.html'; 7 | } 8 | 9 | export { 10 | CrateDatasource as Datasource, 11 | CrateDatasourceQueryCtrl as QueryCtrl, 12 | CrateConfigCtrl as ConfigCtrl, 13 | CrateQueryOptionsCtrl as QueryOptionsCtrl 14 | }; 15 | -------------------------------------------------------------------------------- /src/partials/config.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |

Crate details

7 | 8 |
9 |
10 |
11 | Schema 12 | 15 | 16 |
17 | 18 |
19 | Table 20 | 23 | 24 |
25 |
26 | 27 | 31 | 32 | 33 |
34 | Time column 35 | 39 | 40 |
41 | 42 |
43 | Default grouping interval 44 | 48 |
49 | 50 |
51 | -------------------------------------------------------------------------------- /src/partials/query.editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
43 | 44 |
45 | 54 |
55 | 56 |
57 | 62 | 63 | 68 | 69 |
70 | 71 |
72 | 73 | 77 | 78 |
79 | 80 | 89 | 90 |
91 | 96 | 101 |
102 |
103 | 104 |
105 |
106 | 109 |
110 |
111 | 114 | 115 |
116 |
117 | 118 | 122 | 123 |
124 |
125 |
126 |
127 |
128 | 129 |
130 |
131 | Group By time Interval 132 | 137 |
138 |
139 | 140 |
141 | 142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | -------------------------------------------------------------------------------- /src/partials/query.options.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raintank/crate-datasource/81e4a2e9b7e0c000133de8533d2a14d01fd216e2/src/partials/query.options.html -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Crate", 3 | "id": "crate-datasource", 4 | "type": "datasource", 5 | 6 | "partials": { 7 | "config": "public/app/plugins/datasource/crate/partials/config.html" 8 | }, 9 | 10 | "metrics": true, 11 | "annotations": false, 12 | 13 | "info": { 14 | "description": "Crate SQL Database datasource", 15 | "author": { 16 | "name": "Crate.IO", 17 | "url": "https://crate.io" 18 | }, 19 | "logos": { 20 | "small": "img/crate_logo.png", 21 | "large": "img/crate_logo.png" 22 | }, 23 | "links": [ 24 | {"name": "GitHub", "url": "https://github.com/raintank/crate-datasource"}, 25 | {"name": "MIT License", "url": "https://github.com/raintank/crate-datasource/blob/master/LICENSE"} 26 | ], 27 | "version": "0.5.1", 28 | "updated": "2017-04-05" 29 | }, 30 | 31 | "dependencies": { 32 | "grafanaVersion": "3.x.x", 33 | "plugins": [] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/query_builder.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | // Maximum LIMIT value 6 | let MAX_LIMIT = 100000; 7 | let DEFAULT_LIMIT = 10000; 8 | 9 | export class CrateQueryBuilder { 10 | schema: string; 11 | table: string; 12 | defaultTimeColumn: string; 13 | defaultGroupInterval: string; 14 | 15 | constructor(schema: string, 16 | table: string, 17 | defaultTimeColumn: string, 18 | defaultGroupInterval: string, 19 | private templateSrv) { 20 | this.schema = schema; 21 | this.table = table; 22 | this.defaultTimeColumn = defaultTimeColumn; 23 | this.defaultGroupInterval = defaultGroupInterval; 24 | this.templateSrv = templateSrv; 25 | } 26 | 27 | /** 28 | * Builds Crate SQL query from given target object. 29 | * @param {any} target Target object. 30 | * @param {number} groupInterval Interval for grouping values. 31 | * @param {string} defaultAgg Default aggregation for values. 32 | * @return {string} SQL query. 33 | */ 34 | build(target: any, groupInterval = 0, adhocFilters = [], limit = DEFAULT_LIMIT, defaultAgg='avg') { 35 | let query: string; 36 | let timeExp: string; 37 | 38 | let timeColumn = quoteColumn(this.defaultTimeColumn); 39 | let aggs = getEnabledAggs(target.metricAggs); 40 | let rawAggs = getRawAggs(aggs); 41 | 42 | if (!aggs.length) { return null; } 43 | 44 | if (groupInterval) { 45 | // Manually aggregate by time interval, ie "SELECT floor(ts/10)*10 as time ..." 46 | timeExp = `floor(${timeColumn}/${groupInterval})*${groupInterval}`; 47 | aggs = aggregateMetrics(aggs, 'avg'); 48 | } else { 49 | timeExp = timeColumn; 50 | } 51 | 52 | // SELECT 53 | let renderedAggs = this.renderMetricAggs(aggs); 54 | query = "SELECT " + timeExp + " as time, " + renderedAggs; 55 | 56 | // Add GROUP BY columns to SELECT statement. 57 | if (target.groupByColumns && target.groupByColumns.length) { 58 | query += ", " + target.groupByColumns.join(', '); 59 | } 60 | query += ` FROM "${this.schema}"."${this.table}"` + 61 | ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 62 | 63 | // WHERE 64 | if (target.whereClauses && target.whereClauses.length) { 65 | query += " AND " + this.renderWhereClauses(target.whereClauses); 66 | } 67 | 68 | // Add ad-hoc filters 69 | if (adhocFilters.length > 0) { 70 | query += " AND " + this.renderAdhocFilters(adhocFilters); 71 | } 72 | 73 | // GROUP BY 74 | query += " GROUP BY time"; 75 | if (!groupInterval && rawAggs.length) { 76 | query += ", " + this.renderMetricAggs(rawAggs, false); 77 | } 78 | 79 | if (target.groupByColumns && target.groupByColumns.length) { 80 | query += ", " + target.groupByColumns.join(', '); 81 | } 82 | 83 | // If GROUP BY specified, sort also by selected columns 84 | query += " ORDER BY time"; 85 | if (target.groupByColumns && target.groupByColumns.length) { 86 | query += ", " + target.groupByColumns.join(', '); 87 | } 88 | query += " ASC"; 89 | 90 | return query; 91 | } 92 | 93 | buildAggQuery(target: any, groupInterval = 0, adhocFilters = [], limit?: number) { 94 | let query: string; 95 | let timeExp: string; 96 | 97 | let timeColumn = quoteColumn(this.defaultTimeColumn); 98 | let aggs = getEnabledAggs(target.metricAggs); 99 | aggs = getNotRawAggs(aggs); 100 | 101 | if (!aggs.length) { return null; } 102 | 103 | if (!groupInterval) { 104 | groupInterval = 1; 105 | } 106 | 107 | if (typeof groupInterval === 'number') { 108 | // Manually aggregate by time interval, ie "SELECT floor(ts/10)*10 as time ..." 109 | timeExp = `floor(${timeColumn}/${groupInterval})*${groupInterval}`; 110 | } else { 111 | // Use built-in date_trunc() function 112 | timeExp = `date_trunc('${groupInterval}', ${timeColumn})`; 113 | } 114 | 115 | // SELECT 116 | let renderedAggs = this.renderMetricAggs(aggs); 117 | query = `SELECT ${timeExp} as time, ${renderedAggs}`; 118 | 119 | // Add GROUP BY columns to SELECT statement. 120 | if (target.groupByColumns && target.groupByColumns.length) { 121 | query += ", " + target.groupByColumns.join(', '); 122 | } 123 | 124 | // FROM 125 | query += ` FROM "${this.schema}"."${this.table}"`; 126 | 127 | // WHERE 128 | query += ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 129 | if (target.whereClauses && target.whereClauses.length) { 130 | query += " AND " + this.renderWhereClauses(target.whereClauses); 131 | } 132 | 133 | // Add ad-hoc filters 134 | if (adhocFilters.length > 0) { 135 | query += " AND " + this.renderAdhocFilters(adhocFilters); 136 | } 137 | 138 | // GROUP BY 139 | query += " GROUP BY time"; 140 | 141 | if (target.groupByColumns && target.groupByColumns.length) { 142 | query += ", " + target.groupByColumns.join(', '); 143 | } 144 | 145 | // If GROUP BY specified, sort also by selected columns 146 | query += " ORDER BY time"; 147 | if (target.groupByColumns && target.groupByColumns.length) { 148 | query += ", " + target.groupByColumns.join(', '); 149 | } 150 | query += " ASC"; 151 | 152 | if (limit && limit > DEFAULT_LIMIT) { 153 | limit = Math.min(limit, MAX_LIMIT); 154 | query += ` LIMIT ${limit}`; 155 | } 156 | 157 | return query; 158 | } 159 | 160 | buildRawAggQuery(target: any, groupInterval = 0, adhocFilters = [], limit?: number) { 161 | let query: string; 162 | let timeExp: string; 163 | 164 | let timeColumn = quoteColumn(this.defaultTimeColumn); 165 | let aggs = getEnabledAggs(target.metricAggs); 166 | let rawAggs = getRawAggs(aggs); 167 | 168 | if (!rawAggs.length) { return null; } 169 | 170 | // SELECT 171 | let renderedAggs = this.renderMetricAggs(rawAggs); 172 | query = "SELECT " + timeColumn + " as time, " + renderedAggs; 173 | 174 | // Add GROUP BY columns to SELECT statement. 175 | if (target.groupByColumns && target.groupByColumns.length) { 176 | query += ", " + target.groupByColumns.join(', '); 177 | } 178 | query += ` FROM "${this.schema}"."${this.table}"` + 179 | ` WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?`; 180 | 181 | // WHERE 182 | if (target.whereClauses && target.whereClauses.length) { 183 | query += " AND " + this.renderWhereClauses(target.whereClauses); 184 | } 185 | 186 | // Add ad-hoc filters 187 | if (adhocFilters.length > 0) { 188 | query += " AND " + this.renderAdhocFilters(adhocFilters); 189 | } 190 | 191 | // GROUP BY 192 | query += " GROUP BY time"; 193 | query += ", " + this.renderMetricAggs(rawAggs, false); 194 | 195 | if (target.groupByColumns && target.groupByColumns.length) { 196 | query += ", " + target.groupByColumns.join(', '); 197 | } 198 | 199 | // If GROUP BY specified, sort also by selected columns 200 | query += " ORDER BY time"; 201 | if (target.groupByColumns && target.groupByColumns.length) { 202 | query += ", " + target.groupByColumns.join(', '); 203 | } 204 | query += " ASC"; 205 | 206 | if (limit) { 207 | limit = Math.min(limit, MAX_LIMIT); 208 | query += ` LIMIT ${limit}`; 209 | } 210 | 211 | return query; 212 | } 213 | 214 | renderAdhocFilters(filters) { 215 | let conditions = _.map(filters, (tag, index) => { 216 | let filter_str = ""; 217 | let condition = tag.condition || 'AND'; 218 | let key = quoteColumn(tag.key); 219 | let operator = tag.operator; 220 | let value = quoteValue(tag.value); 221 | 222 | if (index > 0) { 223 | filter_str = `${condition} `; 224 | } 225 | 226 | if (operator === '=~') { 227 | operator = '~'; 228 | } 229 | 230 | filter_str += `${key} ${operator} ${value}` 231 | return filter_str; 232 | }); 233 | return conditions.join(' '); 234 | } 235 | 236 | /** 237 | * Builds SQL query for getting available columns from table. 238 | * @return {string} SQL query. 239 | */ 240 | getColumnsQuery() { 241 | let query = "SELECT column_name " + 242 | "FROM information_schema.columns " + 243 | "WHERE table_schema = '" + this.schema + "' " + 244 | "AND table_name = '" + this.table + "' " + 245 | "ORDER BY 1"; 246 | return query; 247 | } 248 | 249 | getNumericColumnsQuery() { 250 | return "SELECT column_name " + 251 | "FROM information_schema.columns " + 252 | "WHERE table_schema = '" + this.schema + "' " + 253 | "AND table_name = '" + this.table + "' " + 254 | "AND data_type in ('integer', 'long', 'short', 'double', 'float', 'byte') " + 255 | "ORDER BY 1"; 256 | } 257 | 258 | /** 259 | * Builds SQL query for getting unique values for given column. 260 | * @param {string} column Column name 261 | * @param {number} limit Optional. Limit number returned values. 262 | */ 263 | getValuesQuery(column: string, limit?: number, timeRange?) { 264 | let timeColumn = quoteColumn(this.defaultTimeColumn); 265 | let query = `SELECT DISTINCT ${column} ` + 266 | `FROM "${this.schema}"."${this.table}"`; 267 | 268 | if (timeRange) { 269 | let timeFrom = timeRange.from.valueOf(); 270 | let timeTo = timeRange.to.valueOf(); 271 | query += ` WHERE ${timeColumn} >= ${timeFrom} AND ${timeColumn} <= ${timeTo}`; 272 | } 273 | 274 | if (limit) { 275 | query += ` LIMIT ${limit}`; 276 | } 277 | return query; 278 | } 279 | 280 | private renderMetricAggs(metricAggs: any, withAlias=true): string { 281 | let enabledAggs = _.filter(metricAggs, (agg) => { 282 | return !agg.hide; 283 | }); 284 | 285 | let renderedAggs = _.map(enabledAggs, (agg) => { 286 | let alias = ''; 287 | if (agg.alias && withAlias) { 288 | alias = ' AS \"' + agg.alias + '\"'; 289 | } 290 | 291 | let column = quoteColumn(agg.column); 292 | if (agg.type === 'count_distinct') { 293 | return "count(distinct " + column + ")" + alias; 294 | } else if (agg.type === 'raw') { 295 | return column + alias; 296 | } else { 297 | return agg.type + "(" + column + ")" + alias; 298 | } 299 | }); 300 | 301 | if (renderedAggs.length) { 302 | return renderedAggs.join(', '); 303 | } else { 304 | return ""; 305 | } 306 | } 307 | 308 | private renderWhereClauses(whereClauses): string { 309 | let renderedClauses = _.map(whereClauses, (clauseObj, index) => { 310 | let rendered = ""; 311 | if (index !== 0) { 312 | rendered += clauseObj.condition + " "; 313 | } 314 | 315 | // Quote arguments as required by the operator and value type 316 | let rendered_value: string; 317 | let value = clauseObj.value; 318 | if (clauseObj.operator.toLowerCase() === 'in') { 319 | // Handle IN operator. Split comma-separated values. 320 | // "42, 10, a" => 42, 10, 'a' 321 | rendered_value = '(' + _.map(value.split(','), v => { 322 | v = v.trim(); 323 | if (!isNaN(v) || this.containsVariable(v)) { 324 | return v; 325 | } else { 326 | return "'" + v + "'"; 327 | } 328 | }).join(', ') + ')'; 329 | } else { 330 | rendered_value = _.map(value.split(','), v => { 331 | v = v.trim(); 332 | if (!isNaN(v) || this.containsVariable(v)) { 333 | return v; 334 | } else { 335 | return "'" + v + "'"; 336 | } 337 | }).join(', '); 338 | } 339 | rendered += clauseObj.column + ' ' + clauseObj.operator + ' ' + rendered_value; 340 | return rendered; 341 | }); 342 | return renderedClauses.join(' '); 343 | } 344 | 345 | // Check for template variables 346 | private containsVariable(str: string): boolean { 347 | let variables = _.map(this.templateSrv.variables, 'name'); 348 | return _.some(variables, variable => { 349 | let pattern = new RegExp('\\$' + variable); 350 | return pattern.test(str); 351 | }); 352 | } 353 | } 354 | 355 | export function getSchemas() { 356 | var query = "SELECT schema_name " + 357 | "FROM information_schema.schemata " + 358 | "WHERE schema_name NOT IN ('information_schema', 'blob', 'sys', 'pg_catalog') " + 359 | "ORDER BY 1"; 360 | return query; 361 | } 362 | 363 | export function getTables(schema) { 364 | var query = "SELECT table_name " + 365 | "FROM information_schema.tables " + 366 | "WHERE table_schema='" + schema + "' " + 367 | "ORDER BY 1"; 368 | return query; 369 | } 370 | 371 | function quoteColumn(column: string): string { 372 | if (isWithUpperCase(column)) { 373 | return '\"' + column + '\"'; 374 | } else { 375 | return column; 376 | } 377 | } 378 | 379 | function quoteValue(value: string): string { 380 | value = value.trim(); 381 | let match = value.match(/^'?([^']*)'?$/); 382 | if (match[1]) { 383 | value = match[1]; 384 | } else { 385 | return value; 386 | } 387 | 388 | if (!isNaN(Number(value))) { 389 | return value; 390 | } else { 391 | return "'" + value + "'"; 392 | } 393 | } 394 | 395 | function isWithUpperCase(str: string): boolean { 396 | return str.toLowerCase() !== str; 397 | } 398 | 399 | function aggregateMetrics(metricAggs: any, aggType: string) { 400 | let aggs = _.cloneDeep(metricAggs); 401 | return _.map(aggs, agg => { 402 | if (agg.type === 'raw') { 403 | agg.type = aggType; 404 | return agg; 405 | } else { 406 | return agg; 407 | } 408 | }); 409 | } 410 | 411 | export function getEnabledAggs(metricAggs) { 412 | return _.filter(metricAggs, (agg) => { 413 | return !agg.hide; 414 | }); 415 | } 416 | 417 | export function getRawAggs(metricAggs) { 418 | return _.filter(metricAggs, {type: 'raw'}); 419 | } 420 | 421 | export function getNotRawAggs(metricAggs) { 422 | return _.filter(metricAggs, agg => { 423 | return agg.type !== 'raw'; 424 | }); 425 | } 426 | -------------------------------------------------------------------------------- /src/query_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | import {QueryCtrl} from './sdk/sdk'; 6 | import {CrateQueryBuilder} from './query_builder'; 7 | import queryDef from './query_def'; 8 | 9 | export class CrateDatasourceQueryCtrl extends QueryCtrl { 10 | static templateUrl = 'partials/query.editor.html'; 11 | 12 | crateQueryBuilder: CrateQueryBuilder; 13 | groupBySegments: any; 14 | whereSegments: any; 15 | removeWhereSegment: any; 16 | operators: any; 17 | timeIntervals: any[]; 18 | resultFormats: any[]; 19 | 20 | constructor($scope, $injector, private $q, private uiSegmentSrv, private templateSrv) { 21 | super($scope, $injector); 22 | 23 | this.uiSegmentSrv = uiSegmentSrv; 24 | this.templateSrv = templateSrv; 25 | 26 | let ds = this.datasource; 27 | this.crateQueryBuilder = new CrateQueryBuilder(ds.schema, 28 | ds.table, 29 | ds.defaultTimeColumn, 30 | ds.defaultGroupInterval, 31 | templateSrv); 32 | 33 | this.operators = ['<', '>', '<=', '>=', '=', '<>', '!=', 'in', 'like', '~', '!~']; 34 | 35 | this.timeIntervals = [ 36 | {name: 'Auto', value: 'auto'}, 37 | {name: 'Auto (Grafana)', value: 'auto_gf'}, 38 | {name: 'Second', value: 'second'}, 39 | {name: 'Minute', value: 'minute'}, 40 | {name: 'Hour', value: 'hour'}, 41 | {name: 'Day', value: 'day'}, 42 | {name: 'Week', value: 'week'}, 43 | {name: 'Month', value: 'month'}, 44 | {name: 'Quarter', value: 'quarter'}, 45 | {name: 'Year', value: 'year'} 46 | ]; 47 | 48 | this.resultFormats = [ 49 | {text: 'Time series', value: 'time_series'}, 50 | {text: 'Table', value: 'table'}, 51 | ]; 52 | 53 | var target_defaults = { 54 | metricAggs: [ 55 | {type: 'avg', column: 'value'} 56 | ], 57 | groupByColumns: [], 58 | groupByAliases: [], 59 | whereClauses: [], 60 | timeInterval: ds.defaultGroupInterval, 61 | resultFormat: 'time_series' 62 | }; 63 | _.defaults(this.target, target_defaults); 64 | 65 | this.updateGroupByAliases(); 66 | 67 | this.groupBySegments = _.map(this.target.groupByColumns, this.uiSegmentSrv.newSegment); 68 | 69 | // Build WHERE segments 70 | this.whereSegments = []; 71 | this.buildWhereSegments(this.target.whereClauses); 72 | 73 | this.removeWhereSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove --'}); 74 | this.fixSegments(this.groupBySegments); 75 | } 76 | 77 | crateQuery(query, args = []) { 78 | return this.datasource._sql_query(query, args).then(response => { 79 | return response.rows; 80 | }); 81 | } 82 | 83 | getCollapsedText(): string { 84 | if (this.target.rawQuery) { 85 | return this.target.query; 86 | } else { 87 | return this.crateQueryBuilder.build(this.target); 88 | } 89 | } 90 | 91 | //////////////////// 92 | // Event handlers // 93 | //////////////////// 94 | 95 | onChangeInternal(): void { 96 | this.panelCtrl.refresh(); // Asks the panel to refresh data. 97 | } 98 | 99 | groupBySegmentChanged(segment, index): void { 100 | if (segment.type === 'plus-button') { 101 | segment.type = undefined; 102 | } 103 | this.target.groupByColumns = _.map(_.filter(this.groupBySegments, segment => { 104 | return (segment.type !== 'plus-button' && 105 | segment.value !== this.removeWhereSegment.value); 106 | }), 'value'); 107 | this.groupBySegments = _.map(this.target.groupByColumns, this.uiSegmentSrv.newSegment); 108 | this.groupBySegments.push(this.uiSegmentSrv.newPlusButton()); 109 | 110 | if (segment.value === this.removeWhereSegment.value) { 111 | this.target.groupByAliases.splice(index, 1); 112 | } 113 | this.updateGroupByAliases(); 114 | 115 | this.onChangeInternal(); 116 | } 117 | 118 | onGroupByAliasChange(index) { 119 | this.updateGroupByAliases(); 120 | this.onChangeInternal(); 121 | } 122 | 123 | onAggTypeChange(): void { 124 | this.onChangeInternal(); 125 | } 126 | 127 | addMetricAgg(): void { 128 | this.target.metricAggs.push({ type: 'avg', column: 'value' }); 129 | this.onChangeInternal(); 130 | } 131 | 132 | removeMetricAgg(index): void { 133 | this.target.metricAggs.splice(index, 1); 134 | this.onChangeInternal(); 135 | } 136 | 137 | toggleShowMetric(agg): void { 138 | agg.hide = !agg.hide; 139 | this.onChangeInternal(); 140 | } 141 | 142 | toggleEditorMode(): void { 143 | this.target.rawQuery = !this.target.rawQuery; 144 | } 145 | 146 | /////////////////////// 147 | // Query suggestions // 148 | /////////////////////// 149 | 150 | getColumns(allValue?: boolean, onlyNumeric?: boolean) { 151 | let query; 152 | if (onlyNumeric) { 153 | query = this.crateQueryBuilder.getNumericColumnsQuery(); 154 | } else { 155 | query = this.crateQueryBuilder.getColumnsQuery(); 156 | } 157 | let self = this; 158 | return this.crateQuery(query).then(rows => { 159 | if (allValue) { 160 | rows.splice(0, 0, '*'); 161 | } 162 | return self.transformToSegments(_.flatten(rows), true); 163 | }); 164 | } 165 | 166 | getGroupByColumns() { 167 | return this.getColumns().then(columns => { 168 | columns.splice(0, 0, angular.copy(this.removeWhereSegment)); 169 | return columns; 170 | }); 171 | } 172 | 173 | getValues(column, limit = 10) { 174 | let self = this; 175 | let time_range = this.panelCtrl.range; 176 | return this.crateQuery(this.crateQueryBuilder.getValuesQuery(column, limit, time_range)) 177 | .then(rows => { 178 | return self.transformToSegments(_.flatten(rows), true); 179 | }); 180 | } 181 | 182 | getColumnsOrValues(segment, index) { 183 | var self = this; 184 | if (segment.type === 'condition') { 185 | return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); 186 | } 187 | if (segment.type === 'operator') { 188 | return this.$q.when(this.uiSegmentSrv.newOperators(this.operators)); 189 | } 190 | 191 | if (segment.type === 'key' || segment.type === 'plus-button') { 192 | return this.getColumns().then(columns => { 193 | columns.splice(0, 0, angular.copy(this.removeWhereSegment)); 194 | return columns; 195 | }); 196 | } else if (segment.type === 'value') { 197 | return this.getValues(this.whereSegments[index - 2].value); 198 | } 199 | } 200 | 201 | getMetricAggTypes() { 202 | return queryDef.getMetricAggTypes(); 203 | } 204 | 205 | getMetricAggDef(aggType) { 206 | return _.find(this.getMetricAggTypes(), { value: aggType }); 207 | } 208 | 209 | whereSegmentUpdated(segment, index) { 210 | this.whereSegments[index] = segment; 211 | 212 | if (segment.value === this.removeWhereSegment.value) { 213 | this.whereSegments.splice(index, 3); 214 | if (this.whereSegments.length === 0) { 215 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 216 | } else if (this.whereSegments.length > 2) { 217 | this.whereSegments.splice(Math.max(index - 1, 0), 1); 218 | if (this.whereSegments[this.whereSegments.length - 1].type !== 'plus-button') { 219 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 220 | } 221 | } 222 | } else { 223 | if (segment.type === 'plus-button') { 224 | if (index > 2) { 225 | this.whereSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); 226 | } 227 | this.whereSegments.push(this.uiSegmentSrv.newOperator('=')); 228 | this.whereSegments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value')); 229 | segment.type = 'key'; 230 | segment.cssClass = 'query-segment-key'; 231 | } 232 | if ((index + 1) === this.whereSegments.length) { 233 | this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); 234 | } 235 | } 236 | 237 | this.buildWhereClauses(); 238 | 239 | // Refresh only if all fields setted 240 | if (_.every(this.whereSegments, segment => { 241 | return ((segment.value || segment.type === 'plus-button') && 242 | !(segment.fake && segment.type !== 'plus-button')); 243 | })) { 244 | this.panelCtrl.refresh(); 245 | } 246 | } 247 | 248 | /////////////////////// 249 | 250 | buildWhereSegments(whereClauses: any): void { 251 | var self = this; 252 | _.forEach(whereClauses, whereClause => { 253 | if (whereClause.condition) { 254 | self.whereSegments.push(self.uiSegmentSrv.newCondition(whereClause.condition)); 255 | } 256 | self.whereSegments.push(self.uiSegmentSrv.newKey(whereClause.column)); 257 | self.whereSegments.push(self.uiSegmentSrv.newOperator(whereClause.operator)); 258 | self.whereSegments.push(self.uiSegmentSrv.newKeyValue(whereClause.value)); 259 | }); 260 | this.fixSegments(this.whereSegments); 261 | } 262 | 263 | buildWhereClauses() { 264 | var i = 0; 265 | var whereIndex = 0; 266 | var segments = this.whereSegments; 267 | var whereClauses = []; 268 | while (segments.length > i && segments[i].type !== 'plus-button') { 269 | if (whereClauses.length < whereIndex + 1) { 270 | whereClauses.push({condition: '', column: '', operator: '', value: ''}); 271 | } 272 | if (segments[i].type === 'condition') { 273 | whereClauses[whereIndex].condition = segments[i].value; 274 | } else if (segments[i].type === 'key') { 275 | whereClauses[whereIndex].column = segments[i].value; 276 | } else if (segments[i].type === 'operator') { 277 | whereClauses[whereIndex].operator = segments[i].value; 278 | } else if (segments[i].type === 'value') { 279 | whereClauses[whereIndex].value = segments[i].value; 280 | whereIndex++; 281 | } 282 | i++; 283 | } 284 | this.target.whereClauses = whereClauses; 285 | } 286 | 287 | fixSegments(segments) { 288 | var count = segments.length; 289 | var lastSegment = segments[Math.max(count-1, 0)]; 290 | 291 | if (!lastSegment || lastSegment.type !== 'plus-button') { 292 | segments.push(this.uiSegmentSrv.newPlusButton()); 293 | } 294 | } 295 | 296 | transformToSegments(results, addTemplateVars?: boolean) { 297 | var segments = _.map(_.flatten(results), value => { 298 | return this.uiSegmentSrv.newSegment({ 299 | value: value.toString(), 300 | expandable: false 301 | }); 302 | }); 303 | 304 | if (addTemplateVars) { 305 | for (let variable of this.templateSrv.variables) { 306 | segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true })); 307 | } 308 | } 309 | return segments; 310 | } 311 | 312 | updateGroupByAliases() { 313 | let groupByAliases = new Array(this.target.groupByColumns.length); 314 | this.target.groupByColumns.forEach((column, index) => { 315 | if (this.target.groupByAliases[index]) { 316 | groupByAliases[index] = this.target.groupByAliases[index]; 317 | } else { 318 | groupByAliases[index] = ""; 319 | } 320 | }); 321 | this.target.groupByAliases = groupByAliases; 322 | } 323 | 324 | } 325 | -------------------------------------------------------------------------------- /src/query_def.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | let _metricAggTypes = [ 6 | {text: "Raw", value: "raw", allValue: false}, 7 | {text: "Count", value: 'count', allValue: true, anyDataType: true}, 8 | {text: "Distinct Count", value: 'count_distinct', allValue: false, anyDataType: true}, 9 | {text: "Avg / Mean", value: 'avg', allValue: false}, 10 | {text: "Min", value: 'min', allValue: false}, 11 | {text: "Max", value: 'max', allValue: false}, 12 | {text: "Sum", value: 'sum', allValue: false}, 13 | {text: "Geometric Mean", value: 'geometric_mean', allValue: false}, 14 | {text: "Variance", value: 'variance', allValue: false}, 15 | {text: "Std Deviation", value: 'stddev', allValue: false}, 16 | {text: "Arbitrary", value: "arbitrary", allValue: false} 17 | ]; 18 | 19 | export default class QueryDef { 20 | 21 | static getMetricAggTypes() { 22 | return _metricAggTypes; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/response_handler.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import _ from 'lodash'; 4 | 5 | export default function handleResponse(target, result) { 6 | if (target.resultFormat === 'table') { 7 | return handleTableResponse(target, result); 8 | } 9 | 10 | if (target.rawQuery) { 11 | return handleRawResponse(target, result); 12 | } else { 13 | return handleBuildedResponse(target, result); 14 | } 15 | } 16 | 17 | function handleTableResponse(target, result) { 18 | return { 19 | columns: _.map(result.cols, col => { 20 | return {text: col}; 21 | }), 22 | rows: result.rows, 23 | type: 'table' 24 | }; 25 | } 26 | 27 | function handleRawResponse(target, result) { 28 | let columns = result.cols; 29 | let timeColumnIndex = 0; 30 | let valueColumnIndex = 1; 31 | 32 | if (columns.length > 2) { 33 | let groupedResponse = _.groupBy(result.rows, row => { 34 | // Assume row structure is 35 | // [ts, value, ...group by columns] 36 | return row.slice(2).join(' '); 37 | }); 38 | 39 | return _.map(groupedResponse, (rows, key) => { 40 | return { 41 | target: key + ': ' + columns[valueColumnIndex], 42 | datapoints: convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) 43 | }; 44 | }); 45 | } else { 46 | return [{ 47 | target: columns[valueColumnIndex], 48 | datapoints: convertToGrafanaPoints(result.rows, timeColumnIndex, valueColumnIndex) 49 | }]; 50 | } 51 | } 52 | 53 | function handleBuildedResponse(target, result) { 54 | let columns = result.cols; 55 | let timeColumnIndex = 0; 56 | let groupByColumnIndexes: number[], selectColumnIndexes: number[]; 57 | 58 | if (target.groupByColumns.length) { 59 | groupByColumnIndexes = _.map(target.groupByColumns, groupByCol => { 60 | return _.indexOf(columns, groupByCol); 61 | }); 62 | } 63 | 64 | let enabledAggs = _.filter(target.metricAggs, (agg) => { 65 | return !agg.hide; 66 | }); 67 | 68 | if (enabledAggs.length) { 69 | selectColumnIndexes = _.map(enabledAggs, (metricAgg, index) => { 70 | return index + 1; 71 | }); 72 | } else { 73 | return []; 74 | } 75 | 76 | if (groupByColumnIndexes && groupByColumnIndexes.length && !_.some(groupByColumnIndexes, -1)) { 77 | let groupedResponse = _.groupBy(result.rows, row => { 78 | // Construct groupBy key from Group By columns, for example: 79 | // [metric, host] => 'metric host' 80 | return _.map(groupByColumnIndexes, columnIndex => { 81 | return row[columnIndex]; 82 | }).join(' '); 83 | }); 84 | 85 | return _.flatten(_.map(groupedResponse, (rows, key) => { 86 | return _.map(selectColumnIndexes, (valueIndex) => { 87 | let datapoints = convertToGrafanaPoints(rows, timeColumnIndex, valueIndex); 88 | 89 | // Build alias for Group By column values 90 | let group_by_alias: string; 91 | if (rows.length) { 92 | group_by_alias = _.map(groupByColumnIndexes, (columnIndex, i) => { 93 | let first_row = rows[0]; 94 | if (target.groupByAliases && target.groupByAliases[i]) { 95 | let pattern = new RegExp(target.groupByAliases[i]); 96 | let match = pattern.exec(first_row[columnIndex]); 97 | if (match && match.length > 1) { 98 | return match[1]; 99 | } else if (match){ 100 | return match[0]; 101 | } else { 102 | return first_row[columnIndex]; 103 | } 104 | } else { 105 | return first_row[columnIndex]; 106 | } 107 | }).join(' '); 108 | } else { 109 | group_by_alias = key; 110 | } 111 | 112 | return { 113 | target: group_by_alias + ': ' + columns[valueIndex], 114 | datapoints: datapoints 115 | }; 116 | }); 117 | })); 118 | } else { 119 | return _.map(selectColumnIndexes, (valueIndex) => { 120 | let datapoints = convertToGrafanaPoints(result.rows, timeColumnIndex, valueIndex); 121 | 122 | return { 123 | target: columns[valueIndex], 124 | datapoints: datapoints 125 | }; 126 | }); 127 | } 128 | } 129 | 130 | function convertToGrafanaPoints(rows, timeColumnIndex, valueColumnIndex) { 131 | return _.map(rows, row => { 132 | let ts = Number(row[timeColumnIndex]); 133 | let val = row[valueColumnIndex]; 134 | val = val !== null ? Number(val) : null; 135 | 136 | return [val, ts]; 137 | }); 138 | } 139 | 140 | function makeColName(aggType, column) { 141 | if (aggType === 'count_distinct') { 142 | return 'count(DISTINCT ' + column + ')'; 143 | } else { 144 | return aggType + '(' + column + ')'; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/sdk/query_ctrl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | 6 | export class QueryCtrl { 7 | target: any; 8 | datasource: any; 9 | panelCtrl: any; 10 | panel: any; 11 | hasRawMode: boolean; 12 | error: string; 13 | 14 | constructor(public $scope, private $injector) { 15 | this.panel = this.panelCtrl.panel; 16 | } 17 | 18 | refresh() { 19 | this.panelCtrl.refresh(); 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/sdk/sdk.ts: -------------------------------------------------------------------------------- 1 | import {QueryCtrl} from './query_ctrl'; 2 | 3 | import config from 'app/core/config'; 4 | 5 | export function loadPluginCss(options) { 6 | if (config.bootData.user.lightTheme) { 7 | System.import(options.light + '!css'); 8 | } else { 9 | System.import(options.dark + '!css'); 10 | } 11 | } 12 | 13 | export { 14 | QueryCtrl 15 | } 16 | -------------------------------------------------------------------------------- /src/spec/datasource_specs.js: -------------------------------------------------------------------------------- 1 | //import {describe, beforeEach, it, sinon, expect} from '../utils/test_common'; 2 | import {Datasource} from '../module'; 3 | import {convertToCrateInterval} from '../datasource'; 4 | import Q from "q"; 5 | 6 | describe('CrateDatasource', function() { 7 | var ctx = {}; 8 | 9 | describe('When testing datasource', function() { 10 | 11 | beforeEach(function() { 12 | ctx.$q = Q; 13 | ctx.backendSrv = {}; 14 | ctx.templateSrv = {}; 15 | ctx.instanceSettings = { 16 | url: "http://crate.io:4200", 17 | jsonData: { 18 | schema: 'stats', 19 | table: 'nodes', 20 | defaultTimeColumn: 'ts', 21 | defaultGroupInterval: 'minute' 22 | } 23 | }; 24 | ctx.ds = new Datasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); 25 | }); 26 | 27 | it('should return Crate cluster name and version if test ok', function(done) { 28 | ctx.backendSrv.datasourceRequest = function(options) { 29 | return ctx.$q.when({ 30 | status: 200, 31 | data: { 32 | "ok" : true, 33 | "status" : 200, 34 | "name" : "Negasonic Teenage Warhead", 35 | "cluster_name" : "Crate Test Cluster", 36 | "version" : { 37 | "number" : "0.54.8", 38 | "build_hash" : "c3a7dc4caecfe4fef2992148c8b78347d8c30b2e", 39 | "build_timestamp" : "2016-04-08T12:26:00Z", 40 | "build_snapshot" : false, 41 | "es_version" : "1.7.5", 42 | "lucene_version" : "4.10.4" 43 | } 44 | } 45 | }); 46 | }; 47 | 48 | ctx.ds.testDatasource().then(function(result) { 49 | expect(result).to.have.property('status'); 50 | expect(result).to.have.property('title'); 51 | expect(result).to.have.property('message'); 52 | expect(result.status).to.equal('success'); 53 | expect(result.title).to.equal('Success'); 54 | expect(result.message).to.equal('Cluster: Crate Test Cluster, version: 0.54.8'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should return Crate error if host and port was set properly but url is wrong', function(done) { 60 | ctx.backendSrv.datasourceRequest = function(options) { 61 | var deferred = ctx.$q.defer(); 62 | deferred.reject({ 63 | status: 400, 64 | statusText: "Bad Request", 65 | data: "No handler found for uri [/wrong] and method [GET]" 66 | }); 67 | return deferred.promise; 68 | }; 69 | 70 | ctx.ds.testDatasource().then(function(result) { 71 | expect(result).to.have.property('status'); 72 | expect(result).to.have.property('title'); 73 | expect(result).to.have.property('message'); 74 | expect(result.status).to.equal('error'); 75 | expect(result.title).to.equal('Error'); 76 | expect(result.message).to.equal('Bad Request: No handler found for uri [/wrong] and method [GET]'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should return http error if url is unreachable', function(done) { 82 | ctx.backendSrv.datasourceRequest = function(options) { 83 | var deferred = ctx.$q.defer(); 84 | deferred.reject({ 85 | status: 500, 86 | statusText: "Internal Server Error", 87 | data: { 88 | error: "Internal Server Error", 89 | message: "Internal Server Error" 90 | } 91 | }); 92 | return deferred.promise; 93 | }; 94 | 95 | ctx.ds.testDatasource().then(function(result) { 96 | expect(result).to.have.property('status'); 97 | expect(result).to.have.property('title'); 98 | expect(result).to.have.property('message'); 99 | expect(result.status).to.equal('error'); 100 | expect(result.title).to.equal('Error'); 101 | expect(result.message).to.equal('Internal Server Error: Internal Server Error'); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('When converting to Crate interval', function() { 108 | 109 | beforeEach(function() { 110 | }); 111 | 112 | it('should return proper interval for date_trunc() function', function(done) { 113 | var test_map = [ 114 | ['10s', 'second'], 115 | ['20s', 'second'], 116 | ['30s', 'second'], 117 | ['40s', 'second'], 118 | ['50s', 'second'], 119 | ['59s', 'second'], 120 | 121 | ['1m', 'minute'], 122 | ['1h', 'hour'], 123 | ['1d', 'day'], 124 | ['1w', 'week'], 125 | ['1M', 'month'], 126 | ['1y', 'year'] 127 | ]; 128 | 129 | var test_intervals = test_map.map(function(value) { 130 | return value[0]; 131 | }); 132 | var expected_result = test_map.map(function(value) { 133 | return value[1]; 134 | }); 135 | var result = test_intervals.map(convertToCrateInterval); 136 | 137 | expect(result).to.deep.equal(expected_result); 138 | done(); 139 | }); 140 | }); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /src/spec/response_handler_specs.js: -------------------------------------------------------------------------------- 1 | //import {describe, beforeEach, it, sinon, expect} from '../utils/test_common'; 2 | import handleResponse from '../response_handler'; 3 | 4 | describe('Response Handler', function() { 5 | var ctx = {}; 6 | 7 | describe('When handling Crate response', function() { 8 | 9 | beforeEach(function() { 10 | ctx.target = {}; 11 | ctx.crateResponse = {}; 12 | }); 13 | 14 | it('should convert timeseries data to grafana format', function(done) { 15 | ctx.target = { 16 | "groupByColumns": [], 17 | "metricAggs": [ 18 | {"column": "value", "type": "avg"} 19 | ], 20 | "whereClauses": [] 21 | }; 22 | 23 | ctx.crateResponse = { 24 | "cols": ["time","avg(value)"], 25 | "duration": 16, 26 | "rowcount":5, 27 | "rows":[ 28 | [1466640780000,1.2562332153320312], 29 | [1466640840000,1.1889413595199585], 30 | [1466640900000,1.3127131064732869], 31 | [1466640960000,1.3972599903742473], 32 | [1466641020000,1.27950386206309] 33 | ] 34 | }; 35 | 36 | var result = handleResponse(ctx.target, ctx.crateResponse); 37 | expect(result[0].datapoints).to.deep.equal([ 38 | [1.2562332153320312,1466640780000], 39 | [1.1889413595199585,1466640840000], 40 | [1.3127131064732869,1466640900000], 41 | [1.3972599903742473,1466640960000], 42 | [1.27950386206309,1466641020000] 43 | ]); 44 | done(); 45 | }); 46 | 47 | // TODO: find better default metric name 48 | it('should set metric name if no group by columns selected', function(done) { 49 | ctx.target = { 50 | "groupByColumns": [], 51 | "metricAggs": [ 52 | {"column": "value", "type": "avg"} 53 | ], 54 | "whereClauses": [] 55 | }; 56 | 57 | ctx.crateResponse = { 58 | "cols": ["time","avg(value)"], 59 | "duration": 16, 60 | "rowcount":5, 61 | "rows":[ 62 | [1466640780000,1.2562332153320312], 63 | [1466640840000,1.1889413595199585], 64 | [1466640900000,1.3127131064732869], 65 | [1466640960000,1.3972599903742473], 66 | [1466641020000,1.27950386206309] 67 | ] 68 | }; 69 | 70 | var result = handleResponse(ctx.target, ctx.crateResponse); 71 | var series = result[0]; 72 | expect(series).to.have.property('target'); 73 | expect(series).to.have.property('datapoints'); 74 | //expect(series.target).to.equal(""); 75 | done(); 76 | }); 77 | 78 | it('should group response by selected columns', function(done) { 79 | ctx.target = { 80 | "groupByColumns": ["host"], 81 | "groupByAliases": [], 82 | "metricAggs": [ 83 | {"column": "value", "type": "avg"} 84 | ], 85 | "whereClauses": [] 86 | }; 87 | 88 | ctx.crateResponse = { 89 | "cols": ["time","avg(value)","host"], 90 | "duration": 16, 91 | "rowcount":10, 92 | "rows":[ 93 | [1466640780000,1.2562332153320312,"backend01"], 94 | [1466640840000,1.1889413595199585,"backend01"], 95 | [1466640900000,1.3127131064732869,"backend01"], 96 | [1466640960000,1.3972599903742473,"backend01"], 97 | [1466641020000,1.27950386206309,"backend01"], 98 | [1466641080000,1.3942841291427612,"backend02"], 99 | [1466641140000,1.245995541413625,"backend02"], 100 | [1466641200000,1.3355928659439087,"backend02"], 101 | [1466641260000,1.2358959714571636,"backend02"], 102 | [1466641320000,1.4172419905662537,"backend02"] 103 | ] 104 | }; 105 | 106 | var result = handleResponse(ctx.target, ctx.crateResponse); 107 | expect(result.length).to.equal(2); 108 | expect(result[0]).to.have.property('target'); 109 | expect(result[0]).to.have.property('datapoints'); 110 | expect(result[0].target).to.equal('backend01: avg(value)'); 111 | expect(result[1]).to.have.property('target'); 112 | expect(result[1]).to.have.property('datapoints'); 113 | expect(result[1].target).to.equal('backend02: avg(value)'); 114 | done(); 115 | }); 116 | 117 | it('should transform to set of series for all group by columns', function(done) { 118 | ctx.target = { 119 | "groupByColumns": ["host", "metric"], 120 | "groupByAliases": [], 121 | "metricAggs": [ 122 | {"column": "value", "type": "avg"} 123 | ], 124 | "whereClauses": [] 125 | }; 126 | 127 | ctx.crateResponse = { 128 | "cols": ["time","avg(value)","host", "metric"], 129 | "duration": 16, 130 | "rowcount":10, 131 | "rows":[ 132 | [1466640780000,1.2562332153320312,"backend01", "load1"], 133 | [1466640840000,1.1889413595199585,"backend01", "load1"], 134 | 135 | [1466640900000,1.3127131064732869,"backend01", "load5"], 136 | [1466640960000,1.3972599903742473,"backend01", "load5"], 137 | [1466641020000,1.27950386206309, "backend01", "load5"], 138 | 139 | [1466641080000,1.3942841291427612,"backend02", "load1"], 140 | [1466641140000,1.245995541413625, "backend02", "load1"], 141 | 142 | [1466641200000,1.3355928659439087,"backend02", "load5"], 143 | [1466641260000,1.2358959714571636,"backend02", "load5"], 144 | [1466641320000,1.4172419905662537,"backend02", "load5"] 145 | ] 146 | }; 147 | 148 | var result = handleResponse(ctx.target, ctx.crateResponse); 149 | expect(result.length).to.equal(4); 150 | expect(result[0].datapoints).to.deep.equal([ 151 | [1.2562332153320312,1466640780000], 152 | [1.1889413595199585,1466640840000] 153 | ]); 154 | expect(result[0].target).to.equal('backend01 load1: avg(value)'); 155 | done(); 156 | }); 157 | 158 | it('should handle multiple select values', function(done) { 159 | ctx.target = { 160 | "groupByColumns": [], 161 | "metricAggs": [ 162 | {"column": "load1", "type": "avg"}, 163 | {"column": "load15", "type": "avg"} 164 | ], 165 | "whereClauses": [] 166 | }; 167 | 168 | ctx.crateResponse = { 169 | "cols": ["time","avg(load1)","avg(load15)"], 170 | "duration": 16, 171 | "rowcount":5, 172 | "rows":[ 173 | [1466640780000,1.2562332153320312,1.12], 174 | [1466640840000,1.1889413595199585,1.01], 175 | [1466640900000,1.3127131064732869,1.21], 176 | [1466640960000,1.3972599903742473,1.28], 177 | [1466641020000,1.27950386206309,1.16] 178 | ] 179 | }; 180 | 181 | var result = handleResponse(ctx.target, ctx.crateResponse); 182 | expect(result).to.deep.equal([ 183 | { 184 | target: 'avg(load1)', 185 | datapoints: [ 186 | [1.2562332153320312,1466640780000], 187 | [1.1889413595199585,1466640840000], 188 | [1.3127131064732869,1466640900000], 189 | [1.3972599903742473,1466640960000], 190 | [1.27950386206309,1466641020000] 191 | ] 192 | }, 193 | { 194 | target: 'avg(load15)', 195 | datapoints: [ 196 | [1.12,1466640780000], 197 | [1.01,1466640840000], 198 | [1.21,1466640900000], 199 | [1.28,1466640960000], 200 | [1.16,1466641020000] 201 | ] 202 | } 203 | ]); 204 | done(); 205 | }); 206 | 207 | }); 208 | 209 | describe('When handling raw response', function() { 210 | 211 | beforeEach(function() { 212 | ctx.target = {}; 213 | ctx.crateResponse = {}; 214 | }); 215 | 216 | it('should handle GROUP BY columns', function(done) { 217 | ctx.target = { 218 | "rawQuery": true, 219 | "query": "SELECT ts as time, load, hostname " + 220 | "FROM stats.nodes WHERE ts >= ? AND ts <= ? " + 221 | "GROUP BY time, load, hostname " + 222 | "ORDER BY time ASC" 223 | }; 224 | 225 | ctx.crateResponse = { 226 | "cols": ["time", "load", "hostname"], 227 | "duration": 16, 228 | "rowcount":5, 229 | "rows":[ 230 | [1466640780000,1.2562332153320312, "host01"], 231 | [1466640780000,1.2562332153320312, "host02"], 232 | [1466640840000,1.1889413595199585, "host01"], 233 | [1466640840000,1.1889413595199585, "host02"], 234 | [1466640900000,1.3127131064732869, "host01"], 235 | [1466640900000,1.3127131064732869, "host02"], 236 | [1466640960000,1.3972599903742473, "host01"], 237 | [1466640960000,1.3972599903742473, "host02"], 238 | [1466641020000,1.27950386206309, "host01"], 239 | [1466641020000,1.27950386206309, "host02"] 240 | ] 241 | }; 242 | 243 | var result = handleResponse(ctx.target, ctx.crateResponse); 244 | expect(result).to.deep.equal([ 245 | { 246 | target: 'host01: load', 247 | datapoints: [ 248 | [1.2562332153320312,1466640780000], 249 | [1.1889413595199585,1466640840000], 250 | [1.3127131064732869,1466640900000], 251 | [1.3972599903742473,1466640960000], 252 | [1.27950386206309,1466641020000] 253 | ] 254 | }, 255 | { 256 | target: 'host02: load', 257 | datapoints: [ 258 | [1.2562332153320312,1466640780000], 259 | [1.1889413595199585,1466640840000], 260 | [1.3127131064732869,1466640900000], 261 | [1.3972599903742473,1466640960000], 262 | [1.27950386206309,1466641020000] 263 | ] 264 | } 265 | ]); 266 | done(); 267 | }); 268 | }); 269 | 270 | }); 271 | -------------------------------------------------------------------------------- /src/spec/test-main.js: -------------------------------------------------------------------------------- 1 | import prunk from 'prunk'; 2 | import {jsdom} from 'jsdom'; 3 | import chai from 'chai'; 4 | import _ from 'lodash'; 5 | 6 | // Mock angular module 7 | var angularMocks = { 8 | module: function() { 9 | return { 10 | directive: function() { } 11 | }; 12 | } 13 | }; 14 | 15 | var datemathMock = { 16 | parse: function() { } 17 | }; 18 | 19 | var configMock = { 20 | bootData: { 21 | user: { 22 | lightTheme: false 23 | } 24 | } 25 | }; 26 | 27 | // Mock lodash for using with CommonJS in tests. 28 | // Because typescript compiler generates code like this: 29 | // lodash["default"].map() - it uses default export. 30 | var lodashMock = { 31 | default: _ 32 | }; 33 | 34 | // Mock Grafana modules that are not available outside of the core project 35 | // Required for loading module.js 36 | prunk.mock('./css/query-editor.css!', 'no css, dude.'); 37 | prunk.mock('app/plugins/sdk', { 38 | QueryCtrl: null 39 | }); 40 | prunk.mock('app/core/utils/datemath', datemathMock); 41 | prunk.mock('app/core/config', configMock); 42 | prunk.mock('angular', angularMocks); 43 | prunk.mock('jquery', 'module not found'); 44 | prunk.mock('lodash', lodashMock); 45 | 46 | // Setup jsdom 47 | // Required for loading angularjs 48 | global.document = jsdom(''); 49 | global.window = global.document.parentWindow; 50 | global.navigator = window.navigator = {}; 51 | global.Node = window.Node; 52 | 53 | // Setup Chai 54 | chai.should(); 55 | global.assert = chai.assert; 56 | global.expect = chai.expect; 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": true, 5 | "outDir": "dist", 6 | "noImplicitAny": false, 7 | "target": "es3", 8 | "rootDir": "src/", 9 | "sourceRoot": "src/", 10 | "module": "system", 11 | "noEmitOnError": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true 14 | }, 15 | "files": [ 16 | "src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "angular/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "headers", 6 | "bundle": "headers/common.d.ts", 7 | "installed": { 8 | "es6-promise/es6-promise.d.ts": { 9 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 10 | }, 11 | "mocha/mocha.d.ts": { 12 | "commit": "055b3172e8eb374a75826710c4d08677872620d3" 13 | }, 14 | "zone/zone.d.ts": { 15 | "commit": "055b3172e8eb374a75826710c4d08677872620d3" 16 | }, 17 | "rx/rx.d.ts": { 18 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 19 | }, 20 | "rx/rx-lite.d.ts": { 21 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 22 | }, 23 | "angular2/angular2.d.ts": { 24 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 25 | }, 26 | "es6-shim/es6-shim.d.ts": { 27 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 28 | }, 29 | "jquery/jquery.d.ts": { 30 | "commit": "31e7317c9a0793857109236ef7c7f223305a8aa9" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [false, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": false, 8 | "indent": [true, "spaces"], 9 | "label-position": true, 10 | "label-undefined": true, 11 | "max-line-length": [true, 140], 12 | "member-access": false, 13 | "no-arg": true, 14 | "no-bitwise": true, 15 | "no-console": [true, 16 | "debug", 17 | "info", 18 | "time", 19 | "timeEnd", 20 | "trace" 21 | ], 22 | "no-construct": true, 23 | "no-debugger": true, 24 | "no-duplicate-key": true, 25 | "no-duplicate-variable": true, 26 | "no-empty": false, 27 | "no-eval": true, 28 | "no-inferrable-types": true, 29 | "no-shadowed-variable": false, 30 | "no-string-literal": false, 31 | "no-switch-case-fall-through": false, 32 | "no-trailing-whitespace": true, 33 | "no-unused-expression": false, 34 | "no-unused-variable": false, 35 | "no-unreachable": true, 36 | "no-use-before-declare": true, 37 | "no-var-keyword": false, 38 | "object-literal-sort-keys": false, 39 | "one-line": [true, 40 | "check-open-brace", 41 | "check-catch", 42 | "check-else" 43 | ], 44 | "radix": false, 45 | "semicolon": true, 46 | "triple-equals": [true, "allow-null-check"], 47 | "typedef-whitespace": [true, { 48 | "call-signature": "nospace", 49 | "index-signature": "nospace", 50 | "parameter": "nospace", 51 | "property-declaration": "nospace", 52 | "variable-declaration": "nospace" 53 | }], 54 | "variable-name": [true, "ban-keywords"], 55 | "whitespace": [true, 56 | "check-branch", 57 | "check-decl", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | --------------------------------------------------------------------------------