Merge branch 'influxdb_editor_v3'

This commit is contained in:
Torkel Ödegaard
2015-11-30 16:29:20 +01:00
14 changed files with 1351 additions and 349 deletions

View File

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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -0,0 +1,214 @@
///<reference path="../../../headers/common.d.ts" />
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;

View File

@@ -1,4 +1,4 @@
<div class="tight-form-container-no-item-borders">
<div class="">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
@@ -48,98 +48,47 @@
<li>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
</li>
<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery">
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
</div>
</div>
<div ng-hide="target.rawQuery">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="field in target.fields">
<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">SELECT</span>
</li>
<li>
<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
<li ng-repeat="part in selectParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeSelectPart(selectParts, part)" part-updated="selectPartUpdated(selectParts, part)" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li>
<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()" css-class="tight-form-item-large"></metric-segment>
</li>
<li>
<input type="text" class="tight-form-clear-input text-center" style="width: 70px;" ng-model="field.mathExpr" spellcheck='false' placeholder="math expr" ng-blur="get_data()">
</li>
<li class="tight-form-item query-keyword">
AS
</li>
<li>
<input type="text" class="tight-form-clear-input" style="width: 180px;" ng-model="field.asExpr" spellcheck='false' placeholder="as expr" ng-blur="get_data()">
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-show="$index === 0">
<a class="pointer" ng-click="addSelect()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-show="target.fields.length > 1">
<a class="pointer" ng-click="removeSelect($index)"><i class="fa fa-minus"></i></a>
<li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="groupBy in target.groupBy">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">GROUP BY</span>
</li>
<li ng-if="groupBy.type === 'time'">
<span class="tight-form-item">time</span>
<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
</metric-segment>
<li ng-repeat="part in queryModel.groupByParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="get_data();" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li class="dropdown" ng-if="groupBy.type === 'time'">
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
<span ng-show="target.fill">
fill ({{target.fill}})
</span>
<span ng-show="!target.fill">
no fill
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="setFill('')">no fill</a></li>
<li><a ng-click="setFill('0')">fill (0)</a></li>
<li><a ng-click="setFill('null')">fill (null)</a></li>
<li><a ng-click="setFill('none')">fill (none)</a></li>
<li><a ng-click="setFill('previous')">fill (previous)</a></li>
</ul>
</li>
<li ng-if="groupBy.type === 'tag'">
<metric-segment-model property="groupBy.key" get-options="getTagOptions()" on-change="get_data()"></metric-segment>
</li>
</ul>
<ul class="tight-form-list pull-right">
<li class="tight-form-item last" ng-show="$index === 0">
<a class="pointer" ng-click="addGroupBy()"><i class="fa fa-plus"></i></a>
</li>
<li class="tight-form-item last" ng-show="$index > 0">
<a class="pointer" ng-click="removeGroupBy($index)"><i class="fa fa-minus"></i></a>
<li>
<metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>

View File

@@ -0,0 +1,5 @@
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
</div>
<a ng-click="toggleControls()">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>

View File

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

View File

@@ -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;

View File

@@ -0,0 +1,432 @@
///<reference path="../../../headers/common.d.ts" />
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;
}
};

View File

@@ -0,0 +1,178 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('influxQueryPartEditor', function($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
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) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
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();
}
};
});
});

View File

@@ -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');
});
});
});

View File

@@ -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.*/');
});

View File

@@ -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"');
});
});
});

View File

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