-
-
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);
+ });
+ });
+
});
});