feat(influxdb editor): lots of work on new editor, #2856

pull/3375/head
Torkel Ödegaard 10 years ago
parent f053b41645
commit 83052352dc
  1. 141
      public/app/plugins/datasource/influxdb/influx_query.ts
  2. 8
      public/app/plugins/datasource/influxdb/partials/query.editor.html
  3. 3
      public/app/plugins/datasource/influxdb/query_builder.js
  4. 77
      public/app/plugins/datasource/influxdb/query_ctrl.js
  5. 80
      public/app/plugins/datasource/influxdb/query_part.ts
  6. 36
      public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
  7. 66
      public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts
  8. 41
      public/app/plugins/datasource/influxdb/specs/query_part_specs.ts

@ -0,0 +1,141 @@
///<reference path="../../../headers/common.d.ts" />
///<amd-dependency path="./query_builder" name="InfluxQueryBuilder" />
import _ = require('lodash');
import queryPart = require('./query_part');
declare var InfluxQueryBuilder: any;
class InfluxQuery {
target: any;
selectParts: any[];
groupByParts: any;
queryBuilder: any;
constructor(target) {
this.target = target;
target.tags = target.tags || [];
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
target.select = target.select || [[
{name: 'mean', params: ['value']},
]];
this.updateSelectParts();
this.groupByParts = [
queryPart.create({name: 'time', params: ['$interval']})
];
}
updateSelectParts() {
this.selectParts = _.map(this.target.select, function(parts: any) {
return _.map(parts, function(part: any) {
return queryPart.create(part);
});
});
}
removeSelect(index: number) {
this.target.select.splice(index, 1);
this.updateSelectParts();
}
addSelect() {
this.target.select.push([
{name: 'mean', params: ['value']},
]);
this.updateSelectParts();
}
private renderTagCondition(tag, index) {
var str = "";
var operator = tag.operator;
var value = tag.value;
if (index > 0) {
str = (tag.condition || 'AND') + ' ';
}
if (!operator) {
if (/^\/.*\/$/.test(tag.value)) {
operator = '=~';
} else {
operator = '=';
}
}
// quote value unless regex
if (operator !== '=~' && operator !== '!~') {
value = "'" + value + "'";
}
return str + '"' + tag.key + '" ' + operator + ' ' + value;
}
private getGroupByTimeInterval(interval) {
if (interval === 'auto') {
return '$interval';
}
return interval;
}
render() {
var target = this.target;
if (!target.measurement) {
throw "Metric measurement is missing";
}
if (!target.fields) {
target.fields = [{name: 'value', func: target.function || 'mean'}];
}
var query = 'SELECT ';
var i, y;
for (i = 0; i < this.selectParts.length; i++) {
let parts = this.selectParts[i];
var selectText = "";
for (y = 0; y < parts.length; y++) {
let part = parts[y];
selectText = part.render(selectText);
}
if (i > 0) {
query += ', ';
}
query += selectText;
}
var measurement = target.measurement;
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
measurement = '"' + measurement+ '"';
}
query += ' FROM ' + measurement + ' WHERE ';
var conditions = _.map(target.tags, (tag, index) => {
return this.renderTagCondition(tag, index);
});
query += conditions.join(' ');
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
query += ' GROUP BY';
for (i = 0; i < target.groupBy.length; i++) {
var group = target.groupBy[i];
if (group.type === 'time') {
query += ' time(' + this.getGroupByTimeInterval(group.interval) + ')';
} else {
query += ', "' + group.key + '"';
}
}
if (target.fill) {
query += ' fill(' + target.fill + ')';
}
target.query = query;
return query;
}
}
export = InfluxQuery;

@ -65,7 +65,7 @@
<div ng-hide="target.rawQuery">
<div class="tight-form" ng-repeat="parts in selectParts">
<div class="tight-form" ng-repeat="parts in queryModel.selectParts">
<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>
@ -85,13 +85,13 @@
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="groupBy in target.groupBy">
<div class="tight-form" ng-repeat="part in queryModel.groupByParts">
<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'">
<influx-query-part-editor part="groupByParts" class="tight-form-item tight-form-func"></influx-query-part-editor>
<li>
<influx-query-part-editor part="part" class="tight-form-item tight-form-func"></influx-query-part-editor>
</li>
<!-- <li class="dropdown" ng&#45;if="groupBy.type === 'time'"> -->
<!-- <a class="tight&#45;form&#45;item pointer" data&#45;toggle="dropdown" bs&#45;tooltip="'Insert missing values, important when stacking'" data&#45;placement="right"> -->

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

@ -2,10 +2,10 @@ define([
'angular',
'lodash',
'./query_builder',
'./query_part',
'./influx_query',
'./query_part_editor',
],
function (angular, _, InfluxQueryBuilder, queryPart) {
function (angular, _, InfluxQueryBuilder, InfluxQuery) {
'use strict';
var module = angular.module('grafana.controllers');
@ -15,29 +15,18 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
$scope.init = function() {
if (!$scope.target) { return; }
var target = $scope.target;
target.tags = target.tags || [];
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
target.fields = target.fields || [{name: 'value'}];
target.select = target.select || [[
{name: 'field', params: ['value']},
{name: 'mean', params: []},
]];
$scope.target = $scope.target;
$scope.queryModel = new InfluxQuery($scope.target);
$scope.queryBuilder = new InfluxQueryBuilder($scope.target);
$scope.updateSelectParts();
$scope.groupByParts = queryPart.create({name: 'time', params:['$interval']});
$scope.queryBuilder = new InfluxQueryBuilder(target);
if (!target.measurement) {
if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
$scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
$scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
}
$scope.tagSegments = [];
_.each(target.tags, function(tag) {
_.each($scope.target.tags, function(tag) {
if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~";
@ -78,32 +67,14 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
};
$scope.addSelect = function() {
$scope.target.select.push([
{name: 'field', params: ['value']},
{name: 'mean', params: []},
]);
$scope.updateSelectParts();
$scope.queryModel.addSelect();
};
$scope.removeSelect = function(index) {
$scope.target.select.splice(index, 1);
$scope.updateSelectParts();
$scope.queryModel.removeSelect(index);
$scope.get_data();
};
$scope.updateSelectParts = function() {
$scope.selectParts = _.map($scope.target.select, function(parts) {
return _.map(parts, function(part) {
return queryPart.create(part);
});
});
};
$scope.changeFunction = function(func) {
$scope.target.function = func;
$scope.$parent.get_data();
};
$scope.measurementChanged = function() {
$scope.target.measurement = $scope.measurementSegment.value;
$scope.$parent.get_data();
@ -125,22 +96,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
.then($scope.transformToSegments(true), $scope.handleQueryError);
};
$scope.getFunctions = function () {
var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
'stddev', 'first', 'last'
];
return $q.when(_.map(functionList, function(func) {
return uiSegmentSrv.newSegment(func);
}));
};
$scope.getGroupByTimeIntervals = function () {
var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d'];
return $q.when(_.map(times, function(func) {
return uiSegmentSrv.newSegment(func);
}));
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
@ -202,18 +157,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
.then(null, $scope.handleQueryError);
};
$scope.addField = function() {
$scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
_.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
};
$scope.fieldChanged = function(field) {
if (field.name === '-- remove from select --') {
$scope.target.fields = _.without($scope.target.fields, field);
}
$scope.get_data();
};
$scope.getTagOptions = function() {
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');

@ -15,11 +15,13 @@ class QueryPartDef {
name: string;
params: any[];
defaultParams: any[];
renderer: any;
constructor(options: any) {
this.name = options.name;
this.params = options.params;
this.defaultParams = options.defaultParams;
this.renderer = options.renderer;
}
static register(options: any) {
@ -27,25 +29,55 @@ class QueryPartDef {
}
}
QueryPartDef.register({
name: 'field',
category: categories.Transform,
params: [{type: 'field'}],
defaultParams: ['value'],
});
function functionRenderer(part, innerExpr) {
var str = part.def.name + '(';
var parameters = _.map(part.params, (value, index) => {
var paramType = part.def.params[index];
if (paramType.quote === 'single') {
return "'" + value + "'";
} else if (paramType.quote === 'double') {
return '"' + value + '"';
}
return value;
});
if (innerExpr) {
parameters.unshift(innerExpr);
}
return str + parameters.join(', ') + ')';
}
function aliasRenderer(part, innerExpr) {
return innerExpr + ' AS ' + '"' + part.params[0] + '"';
}
function suffixRenderer(part, innerExpr) {
return innerExpr + ' ' + part.params[0];
}
function identityRenderer(part, innerExpr) {
return part.params[0];
}
function quotedIdentityRenderer(part, innerExpr) {
return '"' + part.params[0] + '"';
}
QueryPartDef.register({
name: 'mean',
category: categories.Transform,
params: [],
defaultParams: [],
params: [{type: 'field', quote: 'double'}],
defaultParams: ['value'],
renderer: functionRenderer,
});
QueryPartDef.register({
name: 'derivate',
name: 'derivative',
category: categories.Transform,
params: [{ name: "rate", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
defaultParams: ['10s'],
renderer: functionRenderer,
});
QueryPartDef.register({
@ -53,6 +85,7 @@ QueryPartDef.register({
category: categories.Transform,
params: [{ name: "rate", type: "interval", options: ['$interval', '1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
defaultParams: ['$interval'],
renderer: functionRenderer,
});
QueryPartDef.register({
@ -60,13 +93,16 @@ QueryPartDef.register({
category: categories.Transform,
params: [{ name: "expr", type: "string"}],
defaultParams: [' / 100'],
renderer: suffixRenderer,
});
QueryPartDef.register({
name: 'alias',
category: categories.Transform,
params: [{ name: "name", type: "string"}],
params: [{ name: "name", type: "string", quote: 'double'}],
defaultParams: ['alias'],
renderMode: 'suffix',
renderer: aliasRenderer,
});
class QueryPart {
@ -83,29 +119,11 @@ class QueryPart {
}
this.params = part.params || _.clone(this.def.defaultParams);
this.updateText();
}
render(innerExpr: string) {
var str = this.def.name + '(';
var parameters = _.map(this.params, (value, index) => {
var paramType = this.def.params[index].type;
if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
return value;
}
else if (paramType === 'int_or_interval' && _.isNumber(value)) {
return value;
}
return "'" + value + "'";
});
if (innerExpr) {
parameters.unshift(innerExpr);
}
return str + parameters.join(', ') + ')';
return this.def.renderer(this, innerExpr);
}
hasMultipleParamsInString (strValue, index) {

@ -0,0 +1,36 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import InfluxQuery = require('../influx_query');
describe.only('InfluxQuery', function() {
describe('series with mesurement only', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
describe('series with math and alias', function() {
it('should generate correct query', function() {
var query = new InfluxQuery({
measurement: 'cpu',
select: [
[
{name: 'mean', params: ['value']},
{name: 'math', params: ['/100']},
{name: 'alias', params: ['text']},
]
]
});
var queryText = query.render();
expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
});

@ -9,8 +9,8 @@ describe('InfluxQueryBuilder', function() {
describe('series with mesurement only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}]
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}]
});
var query = builder.build();
@ -22,9 +22,9 @@ describe('InfluxQueryBuilder', function() {
describe('series with math expr and as expr', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
groupBy: [{type: 'time', interval: 'auto'}]
measurement: 'cpu',
fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
groupBy: [{type: 'time', interval: 'auto'}]
});
var query = builder.build();
@ -36,22 +36,22 @@ describe('InfluxQueryBuilder', function() {
describe('series with single tag only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}]
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
+ ' GROUP BY time($interval)');
+ ' GROUP BY time($interval)');
});
it('should switch regex operator with tag value is regex', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'app', value: '/e.*/'}]
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'app', value: '/e.*/'}]
});
var query = builder.build();
@ -62,57 +62,57 @@ describe('InfluxQueryBuilder', function() {
describe('series with multiple fields', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}],
fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}],
fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
});
var query = builder.build();
expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' +
'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
});
});
describe('series with multiple tags only', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
'$timeFilter GROUP BY time($interval)');
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with tags OR condition', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
measurement: 'cpu',
groupBy: [{type: 'time', interval: 'auto'}],
tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
'$timeFilter GROUP BY time($interval)');
'$timeFilter GROUP BY time($interval)');
});
});
describe('series with groupByTag', function() {
it('should generate correct query', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
measurement: 'cpu',
tags: [],
groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
});
var query = builder.build();
expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' +
'GROUP BY time($interval), "host"');
'GROUP BY time($interval), "host"');
});
});
@ -126,8 +126,7 @@ describe('InfluxQueryBuilder', function() {
it('should handle regex measurement in tag keys query', function() {
var builder = new InfluxQueryBuilder({
measurement: '/.*/',
tags: []
measurement: '/.*/', tags: []
});
var query = builder.buildExploreQuery('TAG_KEYS');
expect(query).to.be('SHOW TAG KEYS FROM /.*/');
@ -170,7 +169,10 @@ describe('InfluxQueryBuilder', function() {
});
it('should switch to regex operator in tag condition', function() {
var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]});
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: [{key: 'host', value: '/server.*/'}]
});
var query = builder.buildExploreQuery('TAG_VALUES', 'app');
expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
});

@ -0,0 +1,41 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import queryPart = require('../query_part');
describe('InfluxQueryBuilder', () => {
describe('series with mesurement only', () => {
it('should handle nested function parts', () => {
var part = queryPart.create({
name: 'derivative',
params: ['10s'],
});
expect(part.text).to.be('derivative(10s)');
expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)');
});
it('should handle suffirx parts', () => {
var part = queryPart.create({
name: 'math',
params: ['/ 100'],
});
expect(part.text).to.be('math(/ 100)');
expect(part.render('mean(value)')).to.be('mean(value) / 100');
});
it('should handle alias parts', () => {
var part = queryPart.create({
name: 'alias',
params: ['test'],
});
expect(part.text).to.be('alias(test)');
expect(part.render('mean(value)')).to.be('mean(value) AS "test"');
});
});
});
Loading…
Cancel
Save