Commit 5ca8d590 by Torkel Ödegaard

Working on new query editor for influxdb 0.9, looking good! #1525

parent 11c8e80e
......@@ -14,7 +14,8 @@ function (angular, app, _, $) {
' class="tight-form-clear-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="tight-form-item" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
var buttonTemplate = '<a class="tight-form-item" ng-class="segment.cssClass" ' +
'tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
return {
scope: {
......
......@@ -83,15 +83,16 @@
<li class="tight-form-item query-keyword">
WHERE
</li>
<li>
<input type="text" class="input-medium tight-form-input" ng-model="target.condition"
bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, $index)" on-value-changed="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
<li class="tight-form-item">
<span class="query-keyword">GROUP BY</span>
time($interval), <i class="fa fa-plus"></i>
time($interval)
</li>
<li ng-repeat="segment in groupBySegments">
<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, 0)" on-value-changed="groupByTagUpdated(segment, $index)"></metric-segment>
</li>
<li class="dropdown">
<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">
......
......@@ -33,8 +33,8 @@ function (_) {
query += aggregationFunc + '(value)';
query += ' FROM ' + measurement + ' WHERE $timeFilter';
query += _.map(target.tags, function(value, key) {
return ' AND ' + key + '=' + "'" + value + "'";
query += _.map(target.tags, function(tag) {
return ' AND ' + tag.key + '=' + "'" + tag.value + "'";
}).join('');
query += ' GROUP BY time($interval)';
......
......@@ -7,13 +7,11 @@ function (angular, _) {
var module = angular.module('grafana.controllers');
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) {
module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) {
$scope.functionList = [
'count', 'mean', 'sum', 'min',
'max', 'mode', 'distinct', 'median',
'derivative', 'stddev', 'first', 'last',
'difference'
'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
'derivative', 'stddev', 'first', 'last', 'difference'
];
$scope.functionMenu = _.map($scope.functionList, function(func) {
......@@ -23,12 +21,41 @@ function (angular, _) {
$scope.init = function() {
var target = $scope.target;
target.function = target.function || 'mean';
target.tags = target.tags || [];
target.groupByTags = target.groupByTags || [];
if (!target.measurement) {
$scope.measurementSegment = MetricSegment.newSelectMeasurement();
} else {
$scope.measurementSegment = new MetricSegment(target.measurement);
}
$scope.tagSegments = [];
_.each(target.tags, function(tag) {
if (tag.condition) {
$scope.tagSegments.push(MetricSegment.newCondition(tag.condition));
}
$scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key' }));
$scope.tagSegments.push(new MetricSegment({fake: true, value: "="}));
$scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value'}));
});
if ($scope.tagSegments.length % 3 === 0) {
$scope.tagSegments.push(MetricSegment.newPlusButton());
}
$scope.groupBySegments = [];
_.each(target.groupByTags, function(tag) {
$scope.groupBySegments.push(new MetricSegment(tag));
});
$scope.groupBySegments.push(MetricSegment.newPlusButton());
};
$scope.groupByTagUpdated = function(segment, index) {
if (index === $scope.groupBySegments.length-1) {
$scope.groupBySegments.push(MetricSegment.newPlusButton());
}
};
$scope.changeFunction = function(func) {
......@@ -56,41 +83,105 @@ function (angular, _) {
};
$scope.getMeasurements = function () {
// var measurement = $scope.segments[0].value;
// var queryType, query;
// if (index === 0) {
// queryType = 'MEASUREMENTS';
// query = 'SHOW MEASUREMENTS';
// } else if (index % 2 === 1) {
// queryType = 'TAG_KEYS';
// query = 'SHOW TAG KEYS FROM "' + measurement + '"';
// } else {
// queryType = 'TAG_VALUES';
// query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value;
// }
//
// console.log('getAltSegments: query' , query);
//
console.log('get measurements');
return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) {
console.log('get alt segments: response', results);
var measurements = _.map(results, function(segment) {
return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS')
.then($scope.transformToSegments)
.then($scope.addTemplateVariableSegments)
.then(null, $scope.handleQueryError);
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
};
$scope.transformToSegments = function(results) {
return _.map(results, function(segment) {
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
});
};
$scope.addTemplateVariableSegments = function(segments) {
_.each(templateSrv.variables, function(variable) {
measurements.unshift(new MetricSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
}));
segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
});
return segments;
};
return measurements;
}, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
$scope.getTagsOrValues = function(segment, index) {
var query, queryType;
if (segment.type === 'key' || segment.type === 'plus-button') {
queryType = 'TAG_KEYS';
query = 'SHOW TAG KEYS FROM "' + $scope.target.measurement + '"';
} else if (segment.type === 'value') {
queryType = 'TAG_VALUES';
query = 'SHOW TAG VALUES FROM "' + $scope.target.measurement + '" WITH KEY = ' + $scope.tagSegments[index-2].value;
} else if (segment.type === 'condition') {
return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]);
}
else {
return $q.when([]);
}
return $scope.datasource.metricFindQuery(query, queryType)
.then($scope.transformToSegments)
.then($scope.addTemplateVariableSegments)
.then(function(results) {
if (queryType === 'TAG_KEYS' && segment.type !== 'plus-button') {
results.push(new MetricSegment({fake: true, value: 'remove tag filter'}));
}
return results;
})
.then(null, $scope.handleQueryError);
};
$scope.tagSegmentUpdated = function(segment, index) {
$scope.tagSegments[index] = segment;
if (segment.value === 'remove tag filter') {
$scope.tagSegments.splice(index, 3);
if ($scope.tagSegments.length === 0) {
$scope.tagSegments.push(MetricSegment.newPlusButton());
} else {
$scope.tagSegments.splice(index-1, 1);
$scope.tagSegments.push(MetricSegment.newPlusButton());
}
}
else {
if (segment.type === 'plus-button') {
if (index > 2) {
$scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND'));
}
$scope.tagSegments.push(new MetricSegment({fake: true, value: '=', type: 'operator'}));
$scope.tagSegments.push(new MetricSegment({fake: true, value: 'select tag value', type: 'value' }));
segment.type = 'key';
}
if ((index+1) === $scope.tagSegments.length) {
$scope.tagSegments.push(MetricSegment.newPlusButton());
}
}
$scope.rebuildTargetTagConditions();
};
$scope.rebuildTargetTagConditions = function() {
var tags = [{}];
var tagIndex = 0;
_.each($scope.tagSegments, function(segment2) {
if (segment2.type === 'key') {
tags[tagIndex].key = segment2.value;
}
else if (segment2.type === 'value') {
tags[tagIndex].value = segment2.value;
}
else if (segment2.type === 'condition') {
tags.push({ condition: segment2.value });
tagIndex += 1;
}
});
$scope.target.tags = tags;
$scope.$parent.get_data();
};
function MetricSegment(options) {
......@@ -107,19 +198,25 @@ function (angular, _) {
return;
}
this.cssClass = options.cssClass;
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
MetricSegment.newSelectMeasurement = function() {
return new MetricSegment({value: 'select measurement', fake: true});
};
MetricSegment.newSelectTag = function() {
return new MetricSegment({value: 'select tag', fake: true});
MetricSegment.newCondition = function(condition) {
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
};
MetricSegment.newPlusButton = function() {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus"></i>', type: 'plus-button' });
};
MetricSegment.newSelectTagValue = function() {
......
......@@ -21,7 +21,7 @@ define([
describe('series with tags only', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
tags: {'hostname': 'server1'}
tags: [{key: 'hostname', value: 'server1'}]
});
var query = builder.build();
......
define([
'helpers',
'plugins/datasource/influxdb/queryCtrl'
], function(helpers) {
'use strict';
describe('InfluxDBQueryCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(module('grafana.controllers'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('InfluxQueryCtrl'));
beforeEach(function() {
ctx.scope.target = {};
ctx.scope.$parent = { get_data: sinon.spy() };
ctx.scope.datasource = ctx.datasource;
ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
});
describe('init', function() {
beforeEach(function() {
ctx.scope.init();
});
it('should init tagSegments', function() {
expect(ctx.scope.tagSegments.length).to.be(1);
});
it('should init measurementSegment', function() {
expect(ctx.scope.measurementSegment.value).to.be('select measurement');
});
});
describe('when first tag segment is updated', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
});
it('should update tag key', function() {
expect(ctx.scope.target.tags[0].key).to.be('asd');
expect(ctx.scope.tagSegments[0].type).to.be('key');
});
it('should add tagSegments', function() {
expect(ctx.scope.tagSegments.length).to.be(3);
});
});
describe('when last tag value segment is updated', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
});
it('should update tag value', function() {
expect(ctx.scope.target.tags[0].value).to.be('server1');
});
it('should add plus button for another filter', function() {
expect(ctx.scope.tagSegments[3].fake).to.be(true);
});
});
describe('when second tag key is added', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
});
it('should update tag key', function() {
expect(ctx.scope.target.tags[1].key).to.be('key2');
});
it('should add AND segment', function() {
expect(ctx.scope.tagSegments[3].value).to.be('AND');
});
});
describe('when condition is changed', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
});
it('should update tag condition', function() {
expect(ctx.scope.target.tags[1].condition).to.be('OR');
});
it('should update AND segment', function() {
expect(ctx.scope.tagSegments[3].value).to.be('OR');
expect(ctx.scope.tagSegments.length).to.be(7);
});
});
describe('when deleting is changed', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated({value: 'remove tag filter', type: 'key'}, 4);
});
it('should remove all segment after 2 and replace with plus button', function() {
expect(ctx.scope.tagSegments.length).to.be(4);
expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
});
});
});
});
......@@ -128,6 +128,7 @@ require([
'specs/influxQueryBuilder-specs',
'specs/influx09-querybuilder-specs',
'specs/influxdb-datasource-specs',
'specs/influxdbQueryCtrl-specs',
'specs/graph-ctrl-specs',
'specs/graph-specs',
'specs/graph-tooltip-specs',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment