mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'influxdb_editor_v3'
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
214
public/app/plugins/datasource/influxdb/influx_query.ts
Normal file
214
public/app/plugins/datasource/influxdb/influx_query.ts
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
432
public/app/plugins/datasource/influxdb/query_part.ts
Normal file
432
public/app/plugins/datasource/influxdb/query_part.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
178
public/app/plugins/datasource/influxdb/query_part_editor.js
Normal file
178
public/app/plugins/datasource/influxdb/query_part_editor.js
Normal 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();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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.*/');
|
||||
});
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user