From 83052352dc5f310190449153310cdd9bb7863e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 30 Sep 2015 17:16:34 +0200 Subject: [PATCH] feat(influxdb editor): lots of work on new editor, #2856 --- .../datasource/influxdb/influx_query.ts | 141 ++++++++++++++++++ .../influxdb/partials/query.editor.html | 8 +- .../datasource/influxdb/query_builder.js | 3 +- .../plugins/datasource/influxdb/query_ctrl.js | 77 ++-------- .../plugins/datasource/influxdb/query_part.ts | 80 ++++++---- .../influxdb/specs/influx_query_specs.ts | 36 +++++ .../influxdb/specs/query_builder_specs.ts | 66 ++++---- .../influxdb/specs/query_part_specs.ts | 41 +++++ 8 files changed, 317 insertions(+), 135 deletions(-) create mode 100644 public/app/plugins/datasource/influxdb/influx_query.ts create mode 100644 public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts create mode 100644 public/app/plugins/datasource/influxdb/specs/query_part_specs.ts diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts new file mode 100644 index 00000000000..bdc4ffa8e73 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -0,0 +1,141 @@ +/// +/// + +import _ = require('lodash'); +import queryPart = require('./query_part'); + +declare var InfluxQueryBuilder: any; + +class InfluxQuery { + target: any; + selectParts: any[]; + groupByParts: any; + queryBuilder: any; + + constructor(target) { + this.target = target; + + target.tags = target.tags || []; + target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}]; + target.select = target.select || [[ + {name: 'mean', params: ['value']}, + ]]; + + this.updateSelectParts(); + this.groupByParts = [ + queryPart.create({name: 'time', params: ['$interval']}) + ]; + } + + updateSelectParts() { + this.selectParts = _.map(this.target.select, function(parts: any) { + return _.map(parts, function(part: any) { + return queryPart.create(part); + }); + }); + } + + removeSelect(index: number) { + this.target.select.splice(index, 1); + this.updateSelectParts(); + } + + addSelect() { + this.target.select.push([ + {name: 'mean', params: ['value']}, + ]); + this.updateSelectParts(); + } + + private renderTagCondition(tag, index) { + var str = ""; + var operator = tag.operator; + var value = tag.value; + if (index > 0) { + str = (tag.condition || 'AND') + ' '; + } + + if (!operator) { + if (/^\/.*\/$/.test(tag.value)) { + operator = '=~'; + } else { + operator = '='; + } + } + + // quote value unless regex + if (operator !== '=~' && operator !== '!~') { + value = "'" + value + "'"; + } + + return str + '"' + tag.key + '" ' + operator + ' ' + value; + } + + private getGroupByTimeInterval(interval) { + if (interval === 'auto') { + return '$interval'; + } + return interval; + } + + render() { + var target = this.target; + + if (!target.measurement) { + throw "Metric measurement is missing"; + } + + if (!target.fields) { + target.fields = [{name: 'value', func: target.function || 'mean'}]; + } + + var query = 'SELECT '; + var i, y; + for (i = 0; i < this.selectParts.length; i++) { + let parts = this.selectParts[i]; + var selectText = ""; + for (y = 0; y < parts.length; y++) { + let part = parts[y]; + selectText = part.render(selectText); + } + + if (i > 0) { + query += ', '; + } + query += selectText; + } + + var measurement = target.measurement; + if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { + measurement = '"' + measurement+ '"'; + } + + query += ' FROM ' + measurement + ' WHERE '; + var conditions = _.map(target.tags, (tag, index) => { + return this.renderTagCondition(tag, index); + }); + + query += conditions.join(' '); + query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter'; + + query += ' GROUP BY'; + for (i = 0; i < target.groupBy.length; i++) { + var group = target.groupBy[i]; + if (group.type === 'time') { + query += ' time(' + this.getGroupByTimeInterval(group.interval) + ')'; + } else { + query += ', "' + group.key + '"'; + } + } + + if (target.fill) { + query += ' fill(' + target.fill + ')'; + } + + target.query = query; + + return query; + } +} + +export = InfluxQuery; diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index af40de12302..1993c2d7aa9 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -65,7 +65,7 @@
-
+
  • SELECT @@ -85,13 +85,13 @@
-
+
  • GROUP BY
  • -
  • - +
  • +
  • diff --git a/public/app/plugins/datasource/influxdb/query_builder.js b/public/app/plugins/datasource/influxdb/query_builder.js index 9ab0953b576..84b8b9c692c 100644 --- a/public/app/plugins/datasource/influxdb/query_builder.js +++ b/public/app/plugins/datasource/influxdb/query_builder.js @@ -4,8 +4,9 @@ define([ function (_) { 'use strict'; - function InfluxQueryBuilder(target) { + function InfluxQueryBuilder(target, queryModel) { this.target = target; + this.model = queryModel; if (target.groupByTags) { target.groupBy = [{type: 'time', interval: 'auto'}]; diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.js b/public/app/plugins/datasource/influxdb/query_ctrl.js index d4c4a7f6181..924c7ab77fe 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.js +++ b/public/app/plugins/datasource/influxdb/query_ctrl.js @@ -2,10 +2,10 @@ define([ 'angular', 'lodash', './query_builder', - './query_part', + './influx_query', './query_part_editor', ], -function (angular, _, InfluxQueryBuilder, queryPart) { +function (angular, _, InfluxQueryBuilder, InfluxQuery) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -15,29 +15,18 @@ function (angular, _, InfluxQueryBuilder, queryPart) { $scope.init = function() { if (!$scope.target) { return; } - var target = $scope.target; - target.tags = target.tags || []; - target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}]; - target.fields = target.fields || [{name: 'value'}]; - target.select = target.select || [[ - {name: 'field', params: ['value']}, - {name: 'mean', params: []}, - ]]; + $scope.target = $scope.target; + $scope.queryModel = new InfluxQuery($scope.target); + $scope.queryBuilder = new InfluxQueryBuilder($scope.target); - $scope.updateSelectParts(); - - $scope.groupByParts = queryPart.create({name: 'time', params:['$interval']}); - - $scope.queryBuilder = new InfluxQueryBuilder(target); - - if (!target.measurement) { + if (!$scope.target.measurement) { $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement(); } else { - $scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement); + $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement); } $scope.tagSegments = []; - _.each(target.tags, function(tag) { + _.each($scope.target.tags, function(tag) { if (!tag.operator) { if (/^\/.*\/$/.test(tag.value)) { tag.operator = "=~"; @@ -78,32 +67,14 @@ function (angular, _, InfluxQueryBuilder, queryPart) { }; $scope.addSelect = function() { - $scope.target.select.push([ - {name: 'field', params: ['value']}, - {name: 'mean', params: []}, - ]); - $scope.updateSelectParts(); + $scope.queryModel.addSelect(); }; $scope.removeSelect = function(index) { - $scope.target.select.splice(index, 1); - $scope.updateSelectParts(); + $scope.queryModel.removeSelect(index); $scope.get_data(); }; - $scope.updateSelectParts = function() { - $scope.selectParts = _.map($scope.target.select, function(parts) { - return _.map(parts, function(part) { - return queryPart.create(part); - }); - }); - }; - - $scope.changeFunction = function(func) { - $scope.target.function = func; - $scope.$parent.get_data(); - }; - $scope.measurementChanged = function() { $scope.target.measurement = $scope.measurementSegment.value; $scope.$parent.get_data(); @@ -125,22 +96,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) { .then($scope.transformToSegments(true), $scope.handleQueryError); }; - $scope.getFunctions = function () { - var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', - 'stddev', 'first', 'last' - ]; - return $q.when(_.map(functionList, function(func) { - return uiSegmentSrv.newSegment(func); - })); - }; - - $scope.getGroupByTimeIntervals = function () { - var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d']; - return $q.when(_.map(times, function(func) { - return uiSegmentSrv.newSegment(func); - })); - }; - $scope.handleQueryError = function(err) { $scope.parserError = err.message || 'Failed to issue metric query'; return []; @@ -202,18 +157,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) { .then(null, $scope.handleQueryError); }; - $scope.addField = function() { - $scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'}); - _.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton()); - }; - - $scope.fieldChanged = function(field) { - if (field.name === '-- remove from select --') { - $scope.target.fields = _.without($scope.target.fields, field); - } - $scope.get_data(); - }; - $scope.getTagOptions = function() { var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 7e5dfff3277..9d6d92f569c 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -15,11 +15,13 @@ class QueryPartDef { name: string; params: any[]; defaultParams: any[]; + renderer: any; constructor(options: any) { this.name = options.name; this.params = options.params; this.defaultParams = options.defaultParams; + this.renderer = options.renderer; } static register(options: any) { @@ -27,25 +29,55 @@ class QueryPartDef { } } -QueryPartDef.register({ - name: 'field', - category: categories.Transform, - params: [{type: 'field'}], - defaultParams: ['value'], -}); +function functionRenderer(part, innerExpr) { + var str = part.def.name + '('; + var parameters = _.map(part.params, (value, index) => { + var paramType = part.def.params[index]; + if (paramType.quote === 'single') { + return "'" + value + "'"; + } else if (paramType.quote === 'double') { + return '"' + value + '"'; + } + + return value; + }); + + if (innerExpr) { + parameters.unshift(innerExpr); + } + return str + parameters.join(', ') + ')'; +} + +function aliasRenderer(part, innerExpr) { + return innerExpr + ' AS ' + '"' + part.params[0] + '"'; +} + +function suffixRenderer(part, innerExpr) { + return innerExpr + ' ' + part.params[0]; +} + +function identityRenderer(part, innerExpr) { + return part.params[0]; +} + +function quotedIdentityRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} QueryPartDef.register({ name: 'mean', category: categories.Transform, - params: [], - defaultParams: [], + params: [{type: 'field', quote: 'double'}], + defaultParams: ['value'], + renderer: functionRenderer, }); QueryPartDef.register({ - name: 'derivate', + name: 'derivative', category: categories.Transform, - params: [{ name: "rate", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h'] }], + params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], defaultParams: ['10s'], + renderer: functionRenderer, }); QueryPartDef.register({ @@ -53,6 +85,7 @@ QueryPartDef.register({ category: categories.Transform, params: [{ name: "rate", type: "interval", options: ['$interval', '1s', '10s', '1m', '5min', '10m', '15m', '1h'] }], defaultParams: ['$interval'], + renderer: functionRenderer, }); QueryPartDef.register({ @@ -60,13 +93,16 @@ QueryPartDef.register({ category: categories.Transform, params: [{ name: "expr", type: "string"}], defaultParams: [' / 100'], + renderer: suffixRenderer, }); QueryPartDef.register({ name: 'alias', category: categories.Transform, - params: [{ name: "name", type: "string"}], + params: [{ name: "name", type: "string", quote: 'double'}], defaultParams: ['alias'], + renderMode: 'suffix', + renderer: aliasRenderer, }); class QueryPart { @@ -83,29 +119,11 @@ class QueryPart { } this.params = part.params || _.clone(this.def.defaultParams); + this.updateText(); } render(innerExpr: string) { - var str = this.def.name + '('; - var parameters = _.map(this.params, (value, index) => { - - var paramType = this.def.params[index].type; - if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') { - return value; - } - else if (paramType === 'int_or_interval' && _.isNumber(value)) { - return value; - } - - return "'" + value + "'"; - - }); - - if (innerExpr) { - parameters.unshift(innerExpr); - } - - return str + parameters.join(', ') + ')'; + return this.def.renderer(this, innerExpr); } hasMultipleParamsInString (strValue, index) { diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts new file mode 100644 index 00000000000..9634e327c67 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts @@ -0,0 +1,36 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import InfluxQuery = require('../influx_query'); + +describe.only('InfluxQuery', function() { + + describe('series with mesurement only', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); + }); + }); + + describe('series with math and alias', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [ + [ + {name: 'mean', params: ['value']}, + {name: 'math', params: ['/100']}, + {name: 'alias', params: ['text']}, + ] + ] + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); + }); + }); + +}); diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts index 65a2f453385..5e06ef77db4 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts @@ -9,8 +9,8 @@ describe('InfluxQueryBuilder', function() { describe('series with mesurement only', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - groupBy: [{type: 'time', interval: 'auto'}] + measurement: 'cpu', + groupBy: [{type: 'time', interval: 'auto'}] }); var query = builder.build(); @@ -22,9 +22,9 @@ describe('InfluxQueryBuilder', function() { describe('series with math expr and as expr', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}], - groupBy: [{type: 'time', interval: 'auto'}] + measurement: 'cpu', + fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}], + groupBy: [{type: 'time', interval: 'auto'}] }); var query = builder.build(); @@ -36,22 +36,22 @@ describe('InfluxQueryBuilder', function() { describe('series with single tag only', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - groupBy: [{type: 'time', interval: 'auto'}], - tags: [{key: 'hostname', value: 'server1'}] + measurement: 'cpu', + groupBy: [{type: 'time', interval: 'auto'}], + tags: [{key: 'hostname', value: 'server1'}] }); var query = builder.build(); expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter' - + ' GROUP BY time($interval)'); + + ' GROUP BY time($interval)'); }); it('should switch regex operator with tag value is regex', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - groupBy: [{type: 'time', interval: 'auto'}], - tags: [{key: 'app', value: '/e.*/'}] + measurement: 'cpu', + groupBy: [{type: 'time', interval: 'auto'}], + tags: [{key: 'app', value: '/e.*/'}] }); var query = builder.build(); @@ -62,57 +62,57 @@ describe('InfluxQueryBuilder', function() { describe('series with multiple fields', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - tags: [], - groupBy: [{type: 'time', interval: 'auto'}], - fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }] + measurement: 'cpu', + tags: [], + groupBy: [{type: 'time', interval: 'auto'}], + fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }] }); var query = builder.build(); expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' + - 'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); + 'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); }); }); describe('series with multiple tags only', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - groupBy: [{type: 'time', interval: 'auto'}], - tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}] + measurement: 'cpu', + groupBy: [{type: 'time', interval: 'auto'}], + tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}] }); var query = builder.build(); expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' + - '$timeFilter GROUP BY time($interval)'); + '$timeFilter GROUP BY time($interval)'); }); }); describe('series with tags OR condition', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - groupBy: [{type: 'time', interval: 'auto'}], - tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}] + measurement: 'cpu', + groupBy: [{type: 'time', interval: 'auto'}], + tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}] }); var query = builder.build(); expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' + - '$timeFilter GROUP BY time($interval)'); + '$timeFilter GROUP BY time($interval)'); }); }); describe('series with groupByTag', function() { it('should generate correct query', function() { var builder = new InfluxQueryBuilder({ - measurement: 'cpu', - tags: [], - groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}], + measurement: 'cpu', + tags: [], + groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}], }); var query = builder.build(); expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' + - 'GROUP BY time($interval), "host"'); + 'GROUP BY time($interval), "host"'); }); }); @@ -126,8 +126,7 @@ describe('InfluxQueryBuilder', function() { it('should handle regex measurement in tag keys query', function() { var builder = new InfluxQueryBuilder({ - measurement: '/.*/', - tags: [] + measurement: '/.*/', tags: [] }); var query = builder.buildExploreQuery('TAG_KEYS'); expect(query).to.be('SHOW TAG KEYS FROM /.*/'); @@ -170,7 +169,10 @@ describe('InfluxQueryBuilder', function() { }); it('should switch to regex operator in tag condition', function() { - var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]}); + var builder = new InfluxQueryBuilder({ + measurement: 'cpu', + tags: [{key: 'host', value: '/server.*/'}] + }); var query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); }); diff --git a/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts new file mode 100644 index 00000000000..ea354bf1439 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts @@ -0,0 +1,41 @@ + +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import queryPart = require('../query_part'); + +describe('InfluxQueryBuilder', () => { + + describe('series with mesurement only', () => { + it('should handle nested function parts', () => { + var part = queryPart.create({ + name: 'derivative', + params: ['10s'], + }); + + expect(part.text).to.be('derivative(10s)'); + expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)'); + }); + + it('should handle suffirx parts', () => { + var part = queryPart.create({ + name: 'math', + params: ['/ 100'], + }); + + expect(part.text).to.be('math(/ 100)'); + expect(part.render('mean(value)')).to.be('mean(value) / 100'); + }); + + it('should handle alias parts', () => { + var part = queryPart.create({ + name: 'alias', + params: ['test'], + }); + + expect(part.text).to.be('alias(test)'); + expect(part.render('mean(value)')).to.be('mean(value) AS "test"'); + }); + + }); + +});