+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts
index fd753fb77e1..792aa86f6ee 100644
--- a/public/app/plugins/datasource/graphite/query_ctrl.ts
+++ b/public/app/plugins/datasource/graphite/query_ctrl.ts
@@ -7,11 +7,17 @@ import {Parser} from './parser';
import {QueryCtrl} from 'app/plugins/sdk';
import appEvents from 'app/core/app_events';
+const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
+
export class GraphiteQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
functions: any[];
segments: any[];
+ addTagSegments: any[];
+ tags: any[];
+ seriesByTagUsed: boolean;
+ removeTagValue: string;
/** @ngInject **/
constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
@@ -21,6 +27,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
this.target.target = this.target.target || '';
this.parseTarget();
}
+
+ this.removeTagValue = '-- remove tag --';
}
toggleEditorMode() {
@@ -59,6 +67,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}
this.checkOtherSegments(this.segments.length - 1);
+ this.checkForSeriesByTag();
}
addFunctionParameter(func, value, index, shiftBack) {
@@ -304,6 +313,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
if (!newFunc.params.length && newFunc.added) {
this.targetChanged();
}
+
+ if (newFunc.def.name === 'seriesByTag') {
+ this.parseTarget();
+ }
}
moveAliasFuncLast() {
@@ -333,4 +346,126 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}
}
}
+
+ //////////////////////////////////
+ // Graphite seriesByTag support //
+ //////////////////////////////////
+
+ checkForSeriesByTag() {
+ let seriesByTagFunc = _.find(this.functions, (func) => func.def.name === 'seriesByTag');
+ if (seriesByTagFunc) {
+ this.seriesByTagUsed = true;
+ let tags = this.splitSeriesByTagParams(seriesByTagFunc);
+ this.tags = tags;
+ this.fixTagSegments();
+ }
+ }
+
+ splitSeriesByTagParams(func) {
+ const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
+ return _.flatten(_.map(func.params, (param: string) => {
+ let matches = tagPattern.exec(param);
+ if (matches) {
+ let tag = matches.slice(1);
+ if (tag.length === 3) {
+ return {
+ key: tag[0],
+ operator: tag[1],
+ value: tag[2]
+ }
+ }
+ }
+ return [];
+ }));
+ }
+
+ getTags() {
+ return this.datasource.getTags().then((values) => {
+ let altTags = _.map(values, 'text');
+ altTags.splice(0, 0, this.removeTagValue);
+ return mapToDropdownOptions(altTags);
+ });
+ }
+
+ getTagsAsSegments() {
+ return this.datasource.getTags().then((values) => {
+ return _.map(values, (val) => {
+ return this.uiSegmentSrv.newSegment(val.text);
+ });
+ });
+ }
+
+ getTagOperators() {
+ return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
+ }
+
+ getTagValues(tag) {
+ let tagKey = tag.key;
+ return this.datasource.getTagValues(tagKey).then((values) => {
+ let altValues = _.map(values, 'text');
+ return mapToDropdownOptions(altValues);
+ });
+ }
+
+ tagChanged(tag, tagIndex) {
+ this.error = null;
+
+ if (tag.key === this.removeTagValue) {
+ this.removeTag(tagIndex);
+ return;
+ }
+
+ let newTagParam = renderTagString(tag);
+ this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
+ this.tags[tagIndex] = tag;
+ this.targetChanged();
+ }
+
+ getSeriesByTagFuncIndex() {
+ return _.findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
+ }
+
+ getSeriesByTagFunc() {
+ let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
+ if (seriesByTagFuncIndex >= 0) {
+ return this.functions[seriesByTagFuncIndex];
+ } else {
+ return undefined;
+ }
+ }
+
+ addNewTag(segment) {
+ let newTagKey = segment.value;
+ let newTag = {key: newTagKey, operator: '=', value: 'select tag value'};
+ let newTagParam = renderTagString(newTag);
+ this.getSeriesByTagFunc().params.push(newTagParam);
+ this.tags.push(newTag);
+ this.targetChanged();
+ this.fixTagSegments();
+ }
+
+ removeTag(index) {
+ this.getSeriesByTagFunc().params.splice(index, 1);
+ this.tags.splice(index, 1);
+ this.targetChanged();
+ }
+
+ fixTagSegments() {
+ // Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
+ this.addTagSegments = [this.uiSegmentSrv.newPlusButton()];
+ }
+
+ showDelimiter(index) {
+ return index !== this.tags.length - 1;
+ }
+}
+
+function renderTagString(tag) {
+ return tag.key + tag.operator + tag.value;
+}
+
+function mapToDropdownOptions(results) {
+ return _.map(results, (value) => {
+ return {text: value, value: value};
+ });
}
diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
index bbcd90bd40f..f9eced25642 100644
--- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
+++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
@@ -210,4 +210,113 @@ describe('GraphiteQueryCtrl', function() {
});
});
+ describe('when adding seriesByTag function', function() {
+ beforeEach(function() {
+ ctx.ctrl.target.target = '';
+ ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+ ctx.ctrl.parseTarget();
+ ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
+ });
+
+ it('should update functions', function() {
+ expect(ctx.ctrl.getSeriesByTagFuncIndex()).to.be(0);
+ });
+
+ it('should update seriesByTagUsed flag', function() {
+ expect(ctx.ctrl.seriesByTagUsed).to.be(true);
+ });
+
+ it('should update target', function() {
+ expect(ctx.ctrl.target.target).to.be('seriesByTag()');
+ });
+
+ it('should call refresh', function() {
+ expect(ctx.panelCtrl.refresh.called).to.be(true);
+ });
+ });
+
+ describe('when parsing seriesByTag function', function() {
+ beforeEach(function() {
+ ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+ ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+ ctx.ctrl.parseTarget();
+ });
+
+ it('should add tags', function() {
+ const expected = [
+ {key: 'tag1', operator: '=', value: 'value1'},
+ {key: 'tag2', operator: '!=~', value: 'value2'}
+ ];
+ expect(ctx.ctrl.tags).to.eql(expected);
+ });
+
+ it('should add plus button', function() {
+ expect(ctx.ctrl.addTagSegments.length).to.be(1);
+ });
+ });
+
+ describe('when tag added', function() {
+ beforeEach(function() {
+ ctx.ctrl.target.target = "seriesByTag()";
+ ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+ ctx.ctrl.parseTarget();
+ ctx.ctrl.addNewTag({value: 'tag1'});
+ });
+
+ it('should update tags with default value', function() {
+ const expected = [
+ {key: 'tag1', operator: '=', value: 'select tag value'}
+ ];
+ expect(ctx.ctrl.tags).to.eql(expected);
+ });
+
+ it('should update target', function() {
+ const expected = "seriesByTag('tag1=select tag value')";
+ expect(ctx.ctrl.target.target).to.eql(expected);
+ });
+ });
+
+ describe('when tag changed', function() {
+ beforeEach(function() {
+ ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+ ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+ ctx.ctrl.parseTarget();
+ ctx.ctrl.tagChanged({key: 'tag1', operator: '=', value: 'new_value'}, 0);
+ });
+
+ it('should update tags', function() {
+ const expected = [
+ {key: 'tag1', operator: '=', value: 'new_value'},
+ {key: 'tag2', operator: '!=~', value: 'value2'}
+ ];
+ expect(ctx.ctrl.tags).to.eql(expected);
+ });
+
+ it('should update target', function() {
+ const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
+ expect(ctx.ctrl.target.target).to.eql(expected);
+ });
+ });
+
+ describe('when tag removed', function() {
+ beforeEach(function() {
+ ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+ ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+ ctx.ctrl.parseTarget();
+ ctx.ctrl.removeTag(0);
+ });
+
+ it('should update tags', function() {
+ const expected = [
+ {key: 'tag2', operator: '!=~', value: 'value2'}
+ ];
+ expect(ctx.ctrl.tags).to.eql(expected);
+ });
+
+ it('should update target', function() {
+ const expected = "seriesByTag('tag2!=~value2')";
+ expect(ctx.ctrl.target.target).to.eql(expected);
+ });
+ });
+
});