diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 9babb74dc2d..d8e3fb90c95 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -27,6 +27,7 @@ function (_, $, coreModule) { var segment = $scope.segment; var options = null; var cancelBlur = null; + var linkMode = true; $input.appendTo(elem); $button.appendTo(elem); @@ -55,19 +56,21 @@ function (_, $, coreModule) { }); }; - $scope.switchToLink = function(now) { - if (now === true || cancelBlur) { - clearTimeout(cancelBlur); - cancelBlur = null; - $input.hide(); - $button.show(); - $scope.updateVariableValue($input.val()); - } - else { - // need to have long delay because the blur - // happens long before the click event on the typeahead options - cancelBlur = setTimeout($scope.switchToLink, 100); - } + $scope.switchToLink = function() { + if (linkMode) { return; } + + clearTimeout(cancelBlur); + cancelBlur = null; + linkMode = true; + $input.hide(); + $button.show(); + $scope.updateVariableValue($input.val()); + }; + + $scope.inputBlur = function() { + // happens long before the click event on the typeahead options + // need to have long delay because the blur + cancelBlur = setTimeout($scope.switchToLink, 100); }; $scope.source = function(query, callback) { @@ -98,7 +101,7 @@ function (_, $, coreModule) { } $input.val(value); - $scope.switchToLink(true); + $scope.switchToLink(); return value; }; @@ -139,6 +142,8 @@ function (_, $, coreModule) { $input.show(); $input.focus(); + linkMode = false; + var typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); @@ -146,7 +151,7 @@ function (_, $, coreModule) { } }); - $input.blur($scope.switchToLink); + $input.blur($scope.inputBlur); $compile(elem.contents())($scope); } diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 698abcc51fd..73919ce0e01 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -229,9 +229,9 @@ function (angular, $, _, moment) { var i, j, k; var oldVersion = this.schemaVersion; var panelUpgrades = []; - this.schemaVersion = 7; + this.schemaVersion = 8; - if (oldVersion === 7) { + if (oldVersion === 8) { return; } @@ -342,6 +342,49 @@ function (angular, $, _, moment) { }); } + if (oldVersion < 8) { + panelUpgrades.push(function(panel) { + _.each(panel.targets, function(target) { + // update old influxdb query schema + if (target.fields && target.tags && target.groupBy) { + if (target.rawQuery) { + delete target.fields; + delete target.fill; + } else { + target.select = _.map(target.fields, function(field) { + var parts = []; + parts.push({type: 'field', params: [field.name]}); + parts.push({type: field.func, params: []}); + if (field.mathExpr) { + parts.push({type: 'math', params: [field.mathExpr]}); + } + if (field.asExpr) { + parts.push({type: 'alias', params: [field.asExpr]}); + } + return parts; + }); + delete target.fields; + _.each(target.groupBy, function(part) { + if (part.type === 'time' && part.interval) { + part.params = [part.interval]; + delete part.interval; + } + if (part.type === 'tag' && part.key) { + part.params = [part.key]; + delete part.key; + } + }); + + if (target.fill) { + target.groupBy.push({type: 'fill', params: [target.fill]}); + delete target.fill; + } + } + } + }); + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 87696c17a2a..d066bc142ea 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -3,11 +3,11 @@ define([ 'lodash', 'app/core/utils/datemath', './influx_series', - './query_builder', + './influx_query', './directives', './query_ctrl', ], -function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) { +function (angular, _, dateMath, InfluxSeries, InfluxQuery) { 'use strict'; var module = angular.module('grafana.services'); @@ -41,8 +41,9 @@ function (angular, _, dateMath, InfluxSeries, InfluxQueryBuilder) { queryTargets.push(target); // build query - var queryBuilder = new InfluxQueryBuilder(target); - var query = queryBuilder.build(); + var queryModel = new InfluxQuery(target); + var query = queryModel.render(); + console.log(query); query = query.replace(/\$interval/g, (target.interval || options.interval)); return query; 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..34b86e16930 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -0,0 +1,214 @@ +/// + +import _ = require('lodash'); +import queryPart = require('./query_part'); + +class InfluxQuery { + target: any; + selectModels: any[]; + groupByParts: any; + queryBuilder: any; + + constructor(target) { + this.target = target; + + target.tags = target.tags || []; + target.groupBy = target.groupBy || [ + {type: 'time', params: ['$interval']}, + {type: 'fill', params: ['null']}, + ]; + target.select = target.select || [[ + {type: 'field', params: ['value']}, + {type: 'mean', params: []}, + ]]; + + this.updateProjection(); + } + + updateProjection() { + this.selectModels = _.map(this.target.select, function(parts: any) { + return _.map(parts, queryPart.create); + }); + this.groupByParts = _.map(this.target.groupBy, queryPart.create); + } + + updatePersistedParts() { + this.target.select = _.map(this.selectModels, function(selectParts) { + return _.map(selectParts, function(part: any) { + return {type: part.def.type, params: part.params}; + }); + }); + } + + hasGroupByTime() { + return _.find(this.target.groupBy, (g: any) => g.type === 'time'); + } + + hasFill() { + return _.find(this.target.groupBy, (g: any) => g.type === 'fill'); + } + + addGroupBy(value) { + var stringParts = value.match(/^(\w+)\((.*)\)$/); + var typePart = stringParts[1]; + var arg = stringParts[2]; + var partModel = queryPart.create({type: typePart, params: [arg]}); + var partCount = this.target.groupBy.length; + + if (partCount === 0) { + this.target.groupBy.push(partModel.part); + } else if (typePart === 'time') { + this.target.groupBy.splice(0, 0, partModel.part); + } else if (typePart === 'tag') { + if (this.target.groupBy[partCount-1].type === 'fill') { + this.target.groupBy.splice(partCount-1, 0, partModel.part); + } else { + this.target.groupBy.push(partModel.part); + } + } else { + this.target.groupBy.push(partModel.part); + } + + this.updateProjection(); + } + + removeGroupByPart(part, index) { + var categories = queryPart.getCategories(); + + if (part.def.type === 'time') { + // remove fill + this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill'); + // remove aggregations + this.target.select = _.map(this.target.select, (s: any) => { + return _.filter(s, (part: any) => { + var partModel = queryPart.create(part); + if (partModel.def.category === categories.Aggregations) { + return false; + } + if (partModel.def.category === categories.Selectors) { + return false; + } + return true; + }); + }); + } + + this.target.groupBy.splice(index, 1); + this.updateProjection(); + } + + removeSelect(index: number) { + this.target.select.splice(index, 1); + this.updateProjection(); + } + + removeSelectPart(selectParts, part) { + // if we remove the field remove the whole statement + if (part.def.type === 'field') { + if (this.selectModels.length > 1) { + var modelsIndex = _.indexOf(this.selectModels, selectParts); + this.selectModels.splice(modelsIndex, 1); + } + } else { + var partIndex = _.indexOf(selectParts, part); + selectParts.splice(partIndex, 1); + } + + this.updatePersistedParts(); + } + + addSelectPart(selectParts, type) { + var partModel = queryPart.create({type: type}); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); + } + + 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; + } + + render() { + var target = this.target; + + if (target.rawQuery) { + return target.query; + } + + if (!target.measurement) { + throw "Metric measurement is missing"; + } + + var query = 'SELECT '; + var i, y; + for (i = 0; i < this.selectModels.length; i++) { + let parts = this.selectModels[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'; + + var groupBySection = ""; + for (i = 0; i < this.groupByParts.length; i++) { + var part = this.groupByParts[i]; + if (i > 0) { + // for some reason fill has no seperator + groupBySection += part.def.type === 'fill' ? ' ' : ', '; + } + groupBySection += part.render(''); + } + + if (groupBySection.length) { + query += ' GROUP BY ' + groupBySection; + } + + 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 c9c7203bed7..0aeb8a44224 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,4 +1,4 @@ -
+
  • @@ -48,98 +48,47 @@
  • +
  • + WHERE +
  • +
  • + +
+
-
-
    -
  • - WHERE -
  • -
  • - -
  • -
-
-
-
+
  • SELECT
  • -
  • - +
  • +
  • -
  • - -
  • -
  • - -
  • -
  • - AS -
  • -
  • - -
  • -
- -
    -
  • - -
  • -
  • - +
-
+
- -
    -
  • - -
  • -
  • - +
  • +
diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html new file mode 100644 index 00000000000..0eb0146ec13 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/partials/query_part.html @@ -0,0 +1,5 @@ +
+ +
+ +{{part.def.type}}() diff --git a/public/app/plugins/datasource/influxdb/query_builder.js b/public/app/plugins/datasource/influxdb/query_builder.js index b6b4ae0087c..523003bd70d 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'}]; @@ -92,77 +93,5 @@ function (_) { return query; }; - p._getGroupByTimeInterval = function(interval) { - if (interval === 'auto') { - return '$interval'; - } - return interval; - }; - - p._buildQuery = function() { - 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; - for (i = 0; i < target.fields.length; i++) { - var field = target.fields[i]; - if (i > 0) { - query += ', '; - } - query += field.func + '("' + field.name + '")'; - if (field.mathExpr) { - query += field.mathExpr; - } - if (field.asExpr) { - query += ' AS "' + field.asExpr + '"'; - } else { - query += ' AS "' + field.name + '"'; - } - } - - var measurement = target.measurement; - if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { - measurement = '"' + measurement+ '"'; - } - - query += ' FROM ' + measurement + ' WHERE '; - var conditions = _.map(target.tags, function(tag, index) { - return 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; - }; - - p._modifyRawQuery = function () { - return this.target.query.replace(";", ""); - }; - return InfluxQueryBuilder; }); diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.js b/public/app/plugins/datasource/influxdb/query_ctrl.js index 75dd972fbaa..38f87ecd84e 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.js +++ b/public/app/plugins/datasource/influxdb/query_ctrl.js @@ -2,32 +2,33 @@ define([ 'angular', 'lodash', './query_builder', + './influx_query', + './query_part', + './query_part_editor', ], -function (angular, _, InfluxQueryBuilder) { +function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) { 'use strict'; var module = angular.module('grafana.controllers'); - module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) { + module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) { $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', func: target.function || 'mean'}]; + $scope.target = $scope.target; + $scope.queryModel = new InfluxQuery($scope.target); + $scope.queryBuilder = new InfluxQueryBuilder($scope.target); + $scope.groupBySegment = uiSegmentSrv.newPlusButton(); - $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 = "=~"; @@ -46,9 +47,69 @@ function (angular, _, InfluxQueryBuilder) { }); $scope.fixTagSegments(); + $scope.buildSelectMenu(); $scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'}); }; + $scope.buildSelectMenu = function() { + var categories = queryPart.getCategories(); + $scope.selectMenu = _.reduce(categories, function(memo, cat, key) { + var menu = {text: key}; + menu.submenu = _.map(cat, function(item) { + return {text: item.type, value: item.type}; + }); + memo.push(menu); + return memo; + }, []); + }; + + $scope.getGroupByOptions = function() { + var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); + + return $scope.datasource.metricFindQuery(query) + .then(function(tags) { + var options = []; + if (!$scope.queryModel.hasFill()) { + options.push(uiSegmentSrv.newSegment({value: 'fill(null)'})); + } + if (!$scope.queryModel.hasGroupByTime()) { + options.push(uiSegmentSrv.newSegment({value: 'time($interval)'})); + } + _.each(tags, function(tag) { + options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'})); + }); + return options; + }) + .then(null, $scope.handleQueryError); + }; + + $scope.groupByAction = function() { + $scope.queryModel.addGroupBy($scope.groupBySegment.value); + var plusButton = uiSegmentSrv.newPlusButton(); + $scope.groupBySegment.value = plusButton.value; + $scope.groupBySegment.html = plusButton.html; + $scope.get_data(); + }; + + $scope.removeGroupByPart = function(part, index) { + $scope.queryModel.removeGroupByPart(part, index); + $scope.get_data(); + }; + + $scope.addSelectPart = function(selectParts, cat, subitem) { + $scope.queryModel.addSelectPart(selectParts, subitem.value); + $scope.get_data(); + }; + + $scope.removeSelectPart = function(selectParts, part) { + $scope.queryModel.removeSelectPart(selectParts, part); + $scope.get_data(); + }; + + $scope.selectPartUpdated = function() { + $scope.get_data(); + }; + $scope.fixTagSegments = function() { var count = $scope.tagSegments.length; var lastSegment = $scope.tagSegments[Math.max(count-1, 0)]; @@ -58,38 +119,9 @@ function (angular, _, InfluxQueryBuilder) { } }; - $scope.addGroupBy = function() { - $scope.target.groupBy.push({type: 'tag', key: "select tag"}); - }; - - $scope.removeGroupBy = function(index) { - $scope.target.groupBy.splice(index, 1); - $scope.get_data(); - }; - - $scope.addSelect = function() { - $scope.target.fields.push({name: "select field", func: 'mean'}); - }; - - $scope.removeSelect = function(index) { - $scope.target.fields.splice(index, 1); - $scope.get_data(); - }; - - $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(); - }; - - $scope.getFields = function() { - var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS'); - return $scope.datasource.metricFindQuery(fieldsQuery) - .then($scope.transformToSegments(false), $scope.handleQueryError); + $scope.get_data(); }; $scope.toggleQueryMode = function () { @@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) { .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.getPartOptions = function(part) { + if (part.def.type === 'field') { + var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS'); + return $scope.datasource.metricFindQuery(fieldsQuery) + .then($scope.transformToSegments(true), $scope.handleQueryError); + } + if (part.def.type === 'tag') { + var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); + return $scope.datasource.metricFindQuery(tagsQuery) + .then($scope.transformToSegments(true), $scope.handleQueryError); + } }; $scope.handleQueryError = function(err) { @@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) { .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'); - - return $scope.datasource.metricFindQuery(query) - .then($scope.transformToSegments(false)) - .then(null, $scope.handleQueryError); - }; + }; $scope.setFill = function(fill) { $scope.target.fill = fill; diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts new file mode 100644 index 00000000000..56ea7eecd23 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -0,0 +1,432 @@ +/// + +import _ = require('lodash'); + +var index = []; +var categories = { + Aggregations: [], + Selectors: [], + Transformations: [], + Math: [], + Aliasing: [], + Fields: [], +}; + +var groupByTimeFunctions = []; + +class QueryPartDef { + type: string; + params: any[]; + defaultParams: any[]; + renderer: any; + category: any; + addStrategy: any; + + constructor(options: any) { + this.type = options.type; + this.params = options.params; + this.defaultParams = options.defaultParams; + this.renderer = options.renderer; + this.category = options.category; + this.addStrategy = options.addStrategy; + } + + static register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); + } +} + +function functionRenderer(part, innerExpr) { + var str = part.def.type + '('; + var parameters = _.map(part.params, (value, index) => { + var paramType = part.def.params[index]; + if (paramType.type === 'time') { + if (value === 'auto') { + value = '$interval'; + } + } + 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] + '"'; +} + +function fieldRenderer(part, innerExpr) { + if (part.params[0] === '*') { + return '*'; + } + return '"' + part.params[0] + '"'; +} + +function replaceAggregationAddStrategy(selectParts, partModel) { + // look for existing aggregation + for (var i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Aggregations) { + selectParts[i] = partModel; + return; + } + if (part.def.category === categories.Selectors) { + selectParts[i] = partModel; + return; + } + } + + selectParts.splice(1, 0, partModel); +} + +function addTransformationStrategy(selectParts, partModel) { + var i; + // look for index to add transformation + for (i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Math || part.def.category === categories.Aliasing) { + break; + } + } + + selectParts.splice(i, 0, partModel); +} + +function addMathStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is math, replace it + if (selectParts[partCount-1].def.type === 'math') { + selectParts[partCount-1] = partModel; + return; + } + // if next to last is math, replace it + if (selectParts[partCount-2].def.type === 'math') { + selectParts[partCount-2] = partModel; + return; + } + // if last is alias add it before + else if (selectParts[partCount-1].def.type === 'alias') { + selectParts.splice(partCount-1, 0, partModel); + return; + } + } + selectParts.push(partModel); +} + +function addAliasStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is alias, replace it + if (selectParts[partCount-1].def.type === 'alias') { + selectParts[partCount-1] = partModel; + return; + } + } + selectParts.push(partModel); +} + +function addFieldStrategy(selectParts, partModel, query) { + // copy all parts + var parts = _.map(selectParts, function(part: any) { + return new QueryPart({type: part.def.type, params: _.clone(part.params)}); + }); + + query.selectModels.push(parts); +} + +QueryPartDef.register({ + type: 'field', + addStrategy: addFieldStrategy, + category: categories.Fields, + params: [{type: 'field', dynamicLookup: true}], + defaultParams: ['value'], + renderer: fieldRenderer, +}); + +// Aggregations +QueryPartDef.register({ + type: 'count', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'distinct', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'integral', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'mean', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'median', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'sum', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +// transformations + +QueryPartDef.register({ + type: 'derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'non_negative_derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'stddev', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'time', + category: groupByTimeFunctions, + params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }], + defaultParams: ['auto'], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'fill', + category: groupByTimeFunctions, + params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }], + defaultParams: ['null'], + renderer: functionRenderer, +}); + +// Selectors +QueryPartDef.register({ + type: 'bottom', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{name: 'count', type: 'int'}], + defaultParams: [3], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'first', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'last', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'max', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'min', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'percentile', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{name: 'nth', type: 'int'}], + defaultParams: [95], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'top', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{name: 'count', type: 'int'}], + defaultParams: [3], + renderer: functionRenderer, +}); + +QueryPartDef.register({ + type: 'tag', + category: groupByTimeFunctions, + params: [{name: 'tag', type: 'string', dynamicLookup: true}], + defaultParams: ['tag'], + renderer: fieldRenderer, +}); + +QueryPartDef.register({ + type: 'math', + addStrategy: addMathStrategy, + category: categories.Math, + params: [{ name: "expr", type: "string"}], + defaultParams: [' / 100'], + renderer: suffixRenderer, +}); + +QueryPartDef.register({ + type: 'alias', + addStrategy: addAliasStrategy, + category: categories.Aliasing, + params: [{ name: "name", type: "string", quote: 'double'}], + defaultParams: ['alias'], + renderMode: 'suffix', + renderer: aliasRenderer, +}); + +class QueryPart { + part: any; + def: QueryPartDef; + params: any[]; + text: string; + + constructor(part: any) { + this.part = part; + this.def = index[part.type]; + if (!this.def) { + throw {message: 'Could not find query part ' + part.type}; + } + + part.params = part.params || _.clone(this.def.defaultParams); + this.params = part.params; + this.updateText(); + } + + render(innerExpr: string) { + return this.def.renderer(this, innerExpr); + } + + hasMultipleParamsInString (strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + } + + updateParam (strValue, index) { + // handle optional parameters + // if string contains ',' and next param is optional, split and update both + if (this.hasMultipleParamsInString(strValue, index)) { + _.each(strValue.split(','), function(partVal: string, idx) { + this.updateParam(partVal.trim(), idx); + }, this); + return; + } + + if (strValue === '' && this.def.params[index].optional) { + this.params.splice(index, 1); + } + else { + this.params[index] = strValue; + } + + this.part.params = this.params; + this.updateText(); + } + + updateText() { + if (this.params.length === 0) { + this.text = this.def.type + '()'; + return; + } + + var text = this.def.type + '('; + text += this.params.join(', '); + text += ')'; + this.text = text; + } +} + +export = { + create: function(part): any { + return new QueryPart(part); + }, + + getCategories: function() { + return categories; + } +}; diff --git a/public/app/plugins/datasource/influxdb/query_part_editor.js b/public/app/plugins/datasource/influxdb/query_part_editor.js new file mode 100644 index 00000000000..9d81e315989 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/query_part_editor.js @@ -0,0 +1,178 @@ +define([ + 'angular', + 'lodash', + 'jquery', +], +function (angular, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('influxQueryPartEditor', function($compile, templateSrv) { + + var paramTemplate = ''; + return { + restrict: 'E', + templateUrl: 'app/plugins/datasource/influxdb/partials/query_part.html', + scope: { + part: "=", + removeAction: "&", + partUpdated: "&", + getOptions: "&", + }, + link: function postLink($scope, elem) { + var part = $scope.part; + var partDef = part.def; + var $paramsContainer = elem.find('.query-part-parameters'); + var $controlsContainer = elem.find('.tight-form-func-controls'); + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + var $link = $(this); + var $input = $link.next(); + + $input.val(part.params[paramIndex]); + $input.css('width', ($link.width() + 16) + 'px'); + + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var $input = $(this); + var $link = $input.prev(); + var newValue = $input.val(); + + if (newValue !== '' || part.def.params[paramIndex].optional) { + $link.html(templateSrv.highlightVariablesAsHtml(newValue)); + + part.updateParam($input.val(), paramIndex); + $scope.$apply($scope.partUpdated); + } + + $input.hide(); + $link.show(); + } + + function inputKeyPress(paramIndex, e) { + /*jshint validthis:true */ + if(e.which === 13) { + inputBlur.call(this, paramIndex); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input, param, paramIndex) { + if (!param.options && !param.dynamicLookup) { + return; + } + + var typeaheadSource = function (query, callback) { + if (param.options) { return param.options; } + + $scope.$apply(function() { + $scope.getOptions().then(function(result) { + var dynamicOptions = _.map(result, function(op) { return op.value; }); + callback(dynamicOptions); + }); + }); + }; + + $input.attr('data-provide', 'typeahead'); + var options = param.options; + if (param.type === 'int') { + options = _.map(options, function(val) { return val.toString(); }); + } + + $input.typeahead({ + source: typeaheadSource, + minLength: 0, + items: 1000, + updater: function (value) { + setTimeout(function() { + inputBlur.call($input[0], paramIndex); + }, 0); + return value; + } + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + var items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + } + + $scope.toggleControls = function() { + var targetDiv = elem.closest('.tight-form'); + + if (elem.hasClass('show-function-controls')) { + elem.removeClass('show-function-controls'); + targetDiv.removeClass('has-open-function'); + $controlsContainer.hide(); + return; + } + + elem.addClass('show-function-controls'); + targetDiv.addClass('has-open-function'); + $controlsContainer.show(); + }; + + $scope.removeActionInternal = function() { + $scope.toggleControls(); + $scope.removeAction(); + }; + + function addElementsAndCompile() { + _.each(partDef.params, function(param, index) { + if (param.optional && part.params.length <= index) { + return; + } + + if (index > 0) { + $(', ').appendTo($paramsContainer); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); + var $paramLink = $('' + paramValue + ''); + var $input = $(paramTemplate); + + $paramLink.appendTo($paramsContainer); + $input.appendTo($paramsContainer); + + $input.blur(_.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_.partial(inputKeyPress, index)); + $paramLink.click(_.partial(clickFuncParam, index)); + + addTypeahead($input, param, index); + }); + } + + function relink() { + $paramsContainer.empty(); + addElementsAndCompile(); + } + + relink(); + } + }; + + }); + +}); 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..887bdd9c9b6 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts @@ -0,0 +1,216 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import InfluxQuery = require('../influx_query'); + +describe('InfluxQuery', function() { + + describe('render 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) fill(null)'); + }); + }); + + describe('render series with math and alias', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [ + [ + {type: 'field', params: ['value']}, + {type: 'mean', params: []}, + {type: 'math', params: ['/100']}, + {type: '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) fill(null)'); + }); + }); + + describe('series with single tag only', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time', params: ['auto']}], + tags: [{key: 'hostname', value: 'server1'}] + }); + + var queryText = query.render(); + + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter' + + ' GROUP BY time($interval)'); + }); + + it('should switch regex operator with tag value is regex', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time', params: ['auto']}], + tags: [{key: 'app', value: '/e.*/'}] + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)'); + }); + }); + + describe('series with multiple tags only', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time', params: ['auto']}], + tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}] + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' + + '$timeFilter GROUP BY time($interval)'); + }); + }); + + describe('series with tags OR condition', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time', params: ['auto']}], + tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}] + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' + + '$timeFilter GROUP BY time($interval)'); + }); + }); + + describe('series with groupByTag', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + tags: [], + groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', params: ['host']}], + }); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter ' + + 'GROUP BY time($interval), "host"'); + }); + }); + + describe('render series without group by', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}]], + groupBy: [], + }); + var queryText = query.render(); + expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter'); + }); + }); + + describe('render series without group by and fill', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}]], + groupBy: [{type: 'time'}, {type: 'fill', params: ['0']}], + }); + var queryText = query.render(); + expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(0)'); + }); + }); + + describe('when adding group by part', function() { + + it('should add tag before fill', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time'}, {type: 'fill'}] + }); + + query.addGroupBy('tag(host)'); + expect(query.target.groupBy.length).to.be(3); + expect(query.target.groupBy[1].type).to.be('tag'); + expect(query.target.groupBy[1].params[0]).to.be('host'); + expect(query.target.groupBy[2].type).to.be('fill'); + }); + + it('should add tag last if no fill', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [] + }); + + query.addGroupBy('tag(host)'); + expect(query.target.groupBy.length).to.be(1); + expect(query.target.groupBy[0].type).to.be('tag'); + }); + + }); + + describe('when adding select part', function() { + + it('should add mean after after field', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}]] + }); + + query.addSelectPart(query.selectModels[0], 'mean'); + expect(query.target.select[0].length).to.be(2); + expect(query.target.select[0][1].type).to.be('mean'); + }); + + it('should replace sum by mean', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}, {type: 'mean'}]] + }); + + query.addSelectPart(query.selectModels[0], 'sum'); + expect(query.target.select[0].length).to.be(2); + expect(query.target.select[0][1].type).to.be('sum'); + }); + + it('should add math before alias', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'alias'}]] + }); + + query.addSelectPart(query.selectModels[0], 'math'); + expect(query.target.select[0].length).to.be(4); + expect(query.target.select[0][2].type).to.be('math'); + }); + + it('should add math last', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}, {type: 'mean'}]] + }); + + query.addSelectPart(query.selectModels[0], 'math'); + expect(query.target.select[0].length).to.be(3); + expect(query.target.select[0][2].type).to.be('math'); + }); + + it('should replace math', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'math'}]] + }); + + query.addSelectPart(query.selectModels[0], 'math'); + expect(query.target.select[0].length).to.be(3); + expect(query.target.select[0][2].type).to.be('math'); + }); + + }); + +}); 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..69b7aa0b6cd 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts @@ -6,116 +6,6 @@ declare var InfluxQueryBuilder: any; 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'}] - }); - - var query = builder.build(); - - expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); - }); - }); - - 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'}] - }); - - var query = builder.build(); - - expect(query).to.be('SELECT max("test")*2 AS "new_name" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)'); - }); - }); - - 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'}] - }); - - var query = builder.build(); - - expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter' - + ' 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.*/'}] - }); - - var query = builder.build(); - expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)'); - }); - }); - - 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' }] - }); - - 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)'); - }); - }); - - 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"}] - }); - - 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)'); - }); - }); - - 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"}] - }); - - 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)'); - }); - }); - - 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'}], - }); - - var query = builder.build(); - expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' + - 'GROUP BY time($interval), "host"'); - }); - }); - describe('when building explore queries', function() { it('should only have measurement condition in tag keys query given query with measurement', function() { @@ -126,8 +16,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 +59,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..ee939fdab42 --- /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('InfluxQueryPart', () => { + + describe('series with mesurement only', () => { + it('should handle nested function parts', () => { + var part = queryPart.create({ + type: '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({ + type: '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({ + type: 'alias', + params: ['test'], + }); + + expect(part.text).to.be('alias(test)'); + expect(part.render('mean(value)')).to.be('mean(value) AS "test"'); + }); + + }); + +}); diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index 8da659825b8..5b2fefd384d 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -204,7 +204,7 @@ define([ }); it('dashboard schema version should be set to latest', function() { - expect(model.schemaVersion).to.be(7); + expect(model.schemaVersion).to.be(8); }); }); @@ -248,5 +248,90 @@ define([ expect(clone.meta).to.be(undefined); }); }); + + describe('when loading dashboard with old influxdb query schema', function() { + var model; + var target; + + beforeEach(function() { + model = _dashboardSrv.create({ + rows: [{ + panels: [{ + type: 'graph', + targets: [{ + "alias": "$tag_datacenter $tag_source $col", + "column": "value", + "measurement": "logins.count", + "fields": [ + { + "func": "mean", + "name": "value", + "mathExpr": "*2", + "asExpr": "value" + }, + { + "name": "one-minute", + "func": "mean", + "mathExpr": "*3", + "asExpr": "one-minute" + } + ], + "tags": [], + "fill": "previous", + "function": "mean", + "groupBy": [ + { + "interval": "auto", + "type": "time" + }, + { + "key": "source", + "type": "tag" + }, + { + "type": "tag", + "key": "datacenter" + } + ], + }] + }] + }] + }); + + target = model.rows[0].panels[0].targets[0]; + }); + + it('should update query schema', function() { + expect(target.fields).to.be(undefined); + expect(target.select.length).to.be(2); + expect(target.select[0].length).to.be(4); + expect(target.select[0][0].type).to.be('field'); + expect(target.select[0][1].type).to.be('mean'); + expect(target.select[0][2].type).to.be('math'); + expect(target.select[0][3].type).to.be('alias'); + }); + + }); + + describe('when creating dashboard model with missing list for annoations or templating', function() { + var model; + + beforeEach(function() { + model = _dashboardSrv.create({ + annotations: { + enable: true, + }, + templating: { + enable: true + } + }); + }); + + it('should add empty list', function() { + expect(model.annotations.list.length).to.be(0); + expect(model.templating.list.length).to.be(0); + }); + }); + }); });