-
Data source options
+
diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts
new file mode 100644
index 00000000000..96766d1bbfb
--- /dev/null
+++ b/public/app/features/templating/query_variable.ts
@@ -0,0 +1,167 @@
+///
+
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable';
+import {VariableSrv} from './variable_srv';
+
+function getNoneOption() {
+ return { text: 'None', value: '', isNone: true };
+}
+
+export class QueryVariable implements Variable {
+ datasource: any;
+ query: any;
+ regex: any;
+ sort: any;
+ options: any;
+ current: any;
+ refresh: number;
+ hide: number;
+ name: string;
+ multi: boolean;
+ includeAll: boolean;
+
+ defaults = {
+ type: 'query',
+ query: '',
+ regex: '',
+ sort: 0,
+ datasource: null,
+ refresh: 0,
+ hide: 0,
+ name: '',
+ multi: false,
+ includeAll: false,
+ allValue: null,
+ options: [],
+ current: {},
+ tagsQuery: null,
+ tagValuesQuery: null,
+ };
+
+ constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) {
+ // copy model properties to this instance
+ assignModelProperties(this, model, this.defaults);
+ }
+
+ getModel() {
+ // copy back model properties to model
+ assignModelProperties(this.model, this, this.defaults);
+ return this.model;
+ }
+
+ setValue(option){
+ return this.variableSrv.setOptionAsCurrent(this, option);
+ }
+
+ setValueFromUrl(urlValue) {
+ return this.variableSrv.setOptionFromUrl(this, urlValue);
+ }
+
+ getValueForUrl() {
+ if (this.current.text === 'All') {
+ return 'All';
+ }
+ return this.current.value;
+ }
+
+ updateOptions() {
+ return this.datasourceSrv.get(this.datasource)
+ .then(this.updateOptionsFromMetricFindQuery.bind(this))
+ .then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
+ }
+
+ updateOptionsFromMetricFindQuery(datasource) {
+ return datasource.metricFindQuery(this.query).then(results => {
+ this.options = this.metricNamesToVariableValues(results);
+ if (this.includeAll) {
+ this.addAllOption();
+ }
+ if (!this.options.length) {
+ this.options.push(getNoneOption());
+ }
+ return datasource;
+ });
+ }
+
+ addAllOption() {
+ this.options.unshift({text: 'All', value: "$__all"});
+ }
+
+ metricNamesToVariableValues(metricNames) {
+ var regex, options, i, matches;
+ options = [];
+
+ if (this.regex) {
+ regex = kbn.stringToJsRegex(this.templateSrv.replace(this.regex));
+ }
+
+ for (i = 0; i < metricNames.length; i++) {
+ var item = metricNames[i];
+ var value = item.value || item.text;
+ var text = item.text || item.value;
+
+ if (_.isNumber(value)) {
+ value = value.toString();
+ }
+
+ if (_.isNumber(text)) {
+ text = text.toString();
+ }
+
+ if (regex) {
+ matches = regex.exec(value);
+ if (!matches) { continue; }
+ if (matches.length > 1) {
+ value = matches[1];
+ text = matches[1];
+ }
+ }
+
+ options.push({text: text, value: value});
+ }
+
+ options = _.uniqBy(options, 'value');
+ return this.sortVariableValues(options, this.sort);
+ }
+
+ sortVariableValues(options, sortOrder) {
+ if (sortOrder === 0) {
+ return options;
+ }
+
+ var sortType = Math.ceil(sortOrder / 2);
+ var reverseSort = (sortOrder % 2 === 0);
+
+ if (sortType === 1) {
+ options = _.sortBy(options, 'text');
+ } else if (sortType === 2) {
+ options = _.sortBy(options, function(opt) {
+ var matches = opt.text.match(/.*?(\d+).*/);
+ if (!matches) {
+ return 0;
+ } else {
+ return parseInt(matches[1], 10);
+ }
+ });
+ }
+
+ if (reverseSort) {
+ options = options.reverse();
+ }
+
+ return options;
+ }
+
+ dependsOn(variable) {
+ return containsVariable(this.query, this.datasource, variable.name);
+ }
+}
+
+variableTypes['query'] = {
+ name: 'Query',
+ ctor: QueryVariable,
+ description: 'Variable values are fetched from a datasource query',
+ supportsMulti: true,
+};
diff --git a/public/app/features/templating/specs/adhoc_variable_specs.ts b/public/app/features/templating/specs/adhoc_variable_specs.ts
new file mode 100644
index 00000000000..15856940540
--- /dev/null
+++ b/public/app/features/templating/specs/adhoc_variable_specs.ts
@@ -0,0 +1,40 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import {AdhocVariable} from '../adhoc_variable';
+
+describe('AdhocVariable', function() {
+
+ describe('when serializing to url', function() {
+
+ it('should set return key value and op seperated by pipe', function() {
+ var variable = new AdhocVariable({
+ filters: [
+ {key: 'key1', operator: '=', value: 'value1'},
+ {key: 'key2', operator: '!=', value: 'value2'},
+ ]
+ });
+ var urlValue = variable.getValueForUrl();
+ expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2"]);
+ });
+
+ });
+
+ describe('when deserializing from url', function() {
+
+ it('should restore filters', function() {
+ var variable = new AdhocVariable({});
+ variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2"]);
+
+ expect(variable.filters[0].key).to.be('key1');
+ expect(variable.filters[0].operator).to.be('=');
+ expect(variable.filters[0].value).to.be('value1');
+
+ expect(variable.filters[1].key).to.be('key2');
+ expect(variable.filters[1].operator).to.be('!=');
+ expect(variable.filters[1].value).to.be('value2');
+ });
+
+ });
+
+});
+
diff --git a/public/app/features/templating/specs/query_variable_specs.ts b/public/app/features/templating/specs/query_variable_specs.ts
new file mode 100644
index 00000000000..8a2aef65be2
--- /dev/null
+++ b/public/app/features/templating/specs/query_variable_specs.ts
@@ -0,0 +1,39 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import {QueryVariable} from '../query_variable';
+
+describe('QueryVariable', function() {
+
+ describe('when creating from model', function() {
+
+ it('should set defaults', function() {
+ var variable = new QueryVariable({}, null, null, null, null);
+ expect(variable.datasource).to.be(null);
+ expect(variable.refresh).to.be(0);
+ expect(variable.sort).to.be(0);
+ expect(variable.name).to.be('');
+ expect(variable.hide).to.be(0);
+ expect(variable.options.length).to.be(0);
+ expect(variable.multi).to.be(false);
+ expect(variable.includeAll).to.be(false);
+ });
+
+ it('get model should copy changes back to model', () => {
+ var variable = new QueryVariable({}, null, null, null, null);
+ variable.options = [{text: 'test'}];
+ variable.datasource = 'google';
+ variable.regex = 'asd';
+ variable.sort = 50;
+
+ var model = variable.getModel();
+ expect(model.options.length).to.be(1);
+ expect(model.options[0].text).to.be('test');
+ expect(model.datasource).to.be('google');
+ expect(model.regex).to.be('asd');
+ expect(model.sort).to.be(50);
+ });
+
+ });
+
+});
+
diff --git a/public/app/features/templating/specs/template_srv_specs.ts b/public/app/features/templating/specs/template_srv_specs.ts
new file mode 100644
index 00000000000..94b1e211293
--- /dev/null
+++ b/public/app/features/templating/specs/template_srv_specs.ts
@@ -0,0 +1,237 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import '../all';
+import {Emitter} from 'app/core/core';
+
+describe('templateSrv', function() {
+ var _templateSrv, _variableSrv;
+
+ beforeEach(angularMocks.module('grafana.core'));
+ beforeEach(angularMocks.module('grafana.services'));
+
+ beforeEach(angularMocks.inject(function(variableSrv, templateSrv) {
+ _templateSrv = templateSrv;
+ _variableSrv = variableSrv;
+ }));
+
+ function initTemplateSrv(variables) {
+ _variableSrv.init({
+ templating: {list: variables},
+ events: new Emitter(),
+ });
+ }
+
+ describe('init', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle'}}]);
+ });
+
+ it('should initialize template data', function() {
+ var target = _templateSrv.replace('this.[[test]].filters');
+ expect(target).to.be('this.oogle.filters');
+ });
+ });
+
+ describe('replace can pass scoped vars', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle' }}]);
+ });
+
+ it('should replace $test with scoped value', function() {
+ var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
+ expect(target).to.be('this.mupp.filters');
+ });
+
+ it('should replace $test with scoped text', function() {
+ var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
+ expect(target).to.be('this.asd.filters');
+ });
+ });
+
+ describe('replace can pass multi / all format', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]);
+ });
+
+ it('should replace $test with globbed value', function() {
+ var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
+ expect(target).to.be('this.{value1,value2}.filters');
+ });
+
+ it('should replace $test with piped value', function() {
+ var target = _templateSrv.replace('this=$test', {}, 'pipe');
+ expect(target).to.be('this=value1|value2');
+ });
+
+ it('should replace $test with piped value', function() {
+ var target = _templateSrv.replace('this=$test', {}, 'pipe');
+ expect(target).to.be('this=value1|value2');
+ });
+ });
+
+ describe('variable with all option', function() {
+ beforeEach(function() {
+ initTemplateSrv([{
+ type: 'query',
+ name: 'test',
+ current: {value: '$__all' },
+ options: [
+ {value: '$__all'}, {value: 'value1'}, {value: 'value2'}
+ ]
+ }]);
+ });
+
+ it('should replace $test with formatted all value', function() {
+ var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
+ expect(target).to.be('this.{value1,value2}.filters');
+ });
+ });
+
+ describe('variable with all option and custom value', function() {
+ beforeEach(function() {
+ initTemplateSrv([{
+ type: 'query',
+ name: 'test',
+ current: {value: '$__all' },
+ allValue: '*',
+ options: [
+ {value: 'value1'}, {value: 'value2'}
+ ]
+ }]);
+ });
+
+ it('should replace $test with formatted all value', function() {
+ var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
+ expect(target).to.be('this.*.filters');
+ });
+
+ it('should not escape custom all value', function() {
+ var target = _templateSrv.replace('this.$test', {}, 'regex');
+ expect(target).to.be('this.*');
+ });
+ });
+
+ describe('lucene format', function() {
+ it('should properly escape $test with lucene escape sequences', function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: {value: 'value/4' }}]);
+ var target = _templateSrv.replace('this:$test', {}, 'lucene');
+ expect(target).to.be("this:value\\\/4");
+ });
+ });
+
+ describe('format variable to string values', function() {
+ it('single value should return value', function() {
+ var result = _templateSrv.formatValue('test');
+ expect(result).to.be('test');
+ });
+
+ it('multi value and glob format should render glob string', function() {
+ var result = _templateSrv.formatValue(['test','test2'], 'glob');
+ expect(result).to.be('{test,test2}');
+ });
+
+ it('multi value and lucene should render as lucene expr', function() {
+ var result = _templateSrv.formatValue(['test','test2'], 'lucene');
+ expect(result).to.be('("test" OR "test2")');
+ });
+
+ it('multi value and regex format should render regex string', function() {
+ var result = _templateSrv.formatValue(['test.','test2'], 'regex');
+ expect(result).to.be('(test\\.|test2)');
+ });
+
+ it('multi value and pipe should render pipe string', function() {
+ var result = _templateSrv.formatValue(['test','test2'], 'pipe');
+ expect(result).to.be('test|test2');
+ });
+
+ it('slash should be properly escaped in regex format', function() {
+ var result = _templateSrv.formatValue('Gi3/14', 'regex');
+ expect(result).to.be('Gi3\\/14');
+ });
+
+ });
+
+ describe('can check if variable exists', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
+ });
+
+ it('should return true if exists', function() {
+ var result = _templateSrv.variableExists('$test');
+ expect(result).to.be(true);
+ });
+ });
+
+ describe('can hightlight variables in string', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
+ });
+
+ it('should insert html', function() {
+ var result = _templateSrv.highlightVariablesAsHtml('$test');
+ expect(result).to.be('
$test');
+ });
+
+ it('should insert html anywhere in string', function() {
+ var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
+ expect(result).to.be('this
$test ok');
+ });
+
+ it('should ignore if variables does not exist', function() {
+ var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
+ expect(result).to.be('this $google ok');
+ });
+ });
+
+ describe('updateTemplateData with simple value', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: { value: 'muuuu' } }]);
+ });
+
+ it('should set current value and update template data', function() {
+ var target = _templateSrv.replace('this.[[test]].filters');
+ expect(target).to.be('this.muuuu.filters');
+ });
+ });
+
+ describe('fillVariableValuesForUrl with multi value', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
+ });
+
+ it('should set multiple url params', function() {
+ var params = {};
+ _templateSrv.fillVariableValuesForUrl(params);
+ expect(params['var-test']).to.eql(['val1', 'val2']);
+ });
+ });
+
+ describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
+ beforeEach(function() {
+ initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
+ });
+
+ it('should set scoped value as url params', function() {
+ var params = {};
+ _templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
+ expect(params['var-test']).to.eql('val1');
+ });
+ });
+
+ describe('replaceWithText', function() {
+ beforeEach(function() {
+ initTemplateSrv([
+ {type: 'query', name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
+ {type: 'interval', name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
+ ]);
+ _templateSrv.setGrafanaVariable('$__auto_interval', '13m');
+ _templateSrv.updateTemplateData();
+ });
+
+ it('should replace with text except for grafanaVariables', function() {
+ var target = _templateSrv.replaceWithText('Server: $server, period: $period');
+ expect(target).to.be('Server: All, period: 13m');
+ });
+ });
+});
diff --git a/public/app/features/templating/specs/variable_specs.ts b/public/app/features/templating/specs/variable_specs.ts
new file mode 100644
index 00000000000..9a974eae695
--- /dev/null
+++ b/public/app/features/templating/specs/variable_specs.ts
@@ -0,0 +1,59 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import {containsVariable, assignModelProperties} from '../variable';
+
+describe('containsVariable', function() {
+
+ describe('when checking if a string contains a variable', function() {
+
+ it('should find it with $var syntax', function() {
+ var contains = containsVariable('this.$test.filters', 'test');
+ expect(contains).to.be(true);
+ });
+
+ it('should not find it if only part matches with $var syntax', function() {
+ var contains = containsVariable('this.$ServerDomain.filters', 'Server');
+ expect(contains).to.be(false);
+ });
+
+ it('should find it with [[var]] syntax', function() {
+ var contains = containsVariable('this.[[test]].filters', 'test');
+ expect(contains).to.be(true);
+ });
+
+ it('should find it when part of segment', function() {
+ var contains = containsVariable('metrics.$env.$group-*', 'group');
+ expect(contains).to.be(true);
+ });
+
+ it('should find it its the only thing', function() {
+ var contains = containsVariable('$env', 'env');
+ expect(contains).to.be(true);
+ });
+
+ it('should be able to pass in multiple test strings', function() {
+ var contains = containsVariable('asd','asd2.$env', 'env');
+ expect(contains).to.be(true);
+ });
+
+ });
+
+});
+
+describe('assignModelProperties', function() {
+
+ it('only set properties defined in defaults', function() {
+ var target: any = {test: 'asd'};
+ assignModelProperties(target, {propA: 1, propB: 2}, {propB: 0});
+ expect(target.propB).to.be(2);
+ expect(target.test).to.be('asd');
+ });
+
+ it('use default value if not found on source', function() {
+ var target: any = {test: 'asd'};
+ assignModelProperties(target, {propA: 1, propB: 2}, {propC: 10});
+ expect(target.propC).to.be(10);
+ });
+
+});
+
diff --git a/public/app/features/templating/specs/variable_srv_init_specs.ts b/public/app/features/templating/specs/variable_srv_init_specs.ts
new file mode 100644
index 00000000000..8cac63135ca
--- /dev/null
+++ b/public/app/features/templating/specs/variable_srv_init_specs.ts
@@ -0,0 +1,142 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import '../all';
+
+import _ from 'lodash';
+import helpers from 'test/specs/helpers';
+import {Emitter} from 'app/core/core';
+
+describe('VariableSrv init', function() {
+ var ctx = new helpers.ControllerTestContext();
+
+ beforeEach(angularMocks.module('grafana.core'));
+ beforeEach(angularMocks.module('grafana.controllers'));
+ beforeEach(angularMocks.module('grafana.services'));
+
+ beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
+ beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
+ ctx.$q = $q;
+ ctx.$rootScope = $rootScope;
+ ctx.$location = $location;
+ ctx.variableSrv = $injector.get('variableSrv');
+ ctx.$rootScope.$digest();
+ }));
+
+ function describeInitScenario(desc, fn) {
+ describe(desc, function() {
+ var scenario: any = {
+ urlParams: {},
+ setup: setupFn => {
+ scenario.setupFn = setupFn;
+ }
+ };
+
+ beforeEach(function() {
+ scenario.setupFn();
+ ctx.datasource = {};
+ ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
+
+ ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ctx.datasource));
+ ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
+
+ ctx.$location.search = sinon.stub().returns(scenario.urlParams);
+ ctx.dashboard = {templating: {list: scenario.variables}, events: new Emitter()};
+
+ ctx.variableSrv.init(ctx.dashboard);
+ ctx.$rootScope.$digest();
+
+ scenario.variables = ctx.variableSrv.variables;
+ });
+
+ fn(scenario);
+ });
+ }
+
+ ['query', 'interval', 'custom', 'datasource'].forEach(type => {
+ describeInitScenario('when setting ' + type + ' variable via url', scenario => {
+ scenario.setup(() => {
+ scenario.variables = [{
+ name: 'apps',
+ type: type,
+ current: {text: "test", value: "test"},
+ options: [{text: "test", value: "test"}]
+ }];
+ scenario.urlParams["var-apps"] = "new";
+ });
+
+ it('should update current value', () => {
+ expect(scenario.variables[0].current.value).to.be("new");
+ expect(scenario.variables[0].current.text).to.be("new");
+ });
+ });
+
+ });
+
+ describe('given dependent variables', () => {
+ var variableList = [
+ {
+ name: 'app',
+ type: 'query',
+ query: '',
+ current: {text: "app1", value: "app1"},
+ options: [{text: "app1", value: "app1"}]
+ },
+ {
+ name: 'server',
+ type: 'query',
+ refresh: 1,
+ query: '$app.*',
+ current: {text: "server1", value: "server1"},
+ options: [{text: "server1", value: "server1"}]
+ },
+ ];
+
+ describeInitScenario('when setting parent var from url', scenario => {
+ scenario.setup(() => {
+ scenario.variables = _.cloneDeep(variableList);
+ scenario.urlParams["var-app"] = "google";
+ scenario.queryResult = [{text: 'google-server1'}, {text: 'google-server2'}];
+ });
+
+ it('should update child variable', () => {
+ expect(scenario.variables[1].options.length).to.be(2);
+ expect(scenario.variables[1].current.text).to.be("google-server1");
+ });
+
+ it('should only update it once', () => {
+ expect(ctx.datasource.metricFindQuery.callCount).to.be(1);
+ });
+
+ });
+ });
+
+ describeInitScenario('when template variable is present in url multiple times', scenario => {
+ scenario.setup(() => {
+ scenario.variables = [{
+ name: 'apps',
+ type: 'query',
+ multi: true,
+ current: {text: "val1", value: "val1"},
+ options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
+ }];
+ scenario.urlParams["var-apps"] = ["val2", "val1"];
+ });
+
+ it('should update current value', function() {
+ var variable = ctx.variableSrv.variables[0];
+ expect(variable.current.value.length).to.be(2);
+ expect(variable.current.value[0]).to.be("val2");
+ expect(variable.current.value[1]).to.be("val1");
+ expect(variable.current.text).to.be("val2 + val1");
+ expect(variable.options[0].selected).to.be(true);
+ expect(variable.options[1].selected).to.be(true);
+ });
+
+ it('should set options that are not in value to selected false', function() {
+ var variable = ctx.variableSrv.variables[0];
+ expect(variable.options[2].selected).to.be(false);
+ });
+ });
+
+});
+
diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts
new file mode 100644
index 00000000000..85bca8d6068
--- /dev/null
+++ b/public/app/features/templating/specs/variable_srv_specs.ts
@@ -0,0 +1,395 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+
+import '../all';
+
+import moment from 'moment';
+import helpers from 'test/specs/helpers';
+import {Emitter} from 'app/core/core';
+
+describe('VariableSrv', function() {
+ var ctx = new helpers.ControllerTestContext();
+
+ beforeEach(angularMocks.module('grafana.core'));
+ beforeEach(angularMocks.module('grafana.controllers'));
+ beforeEach(angularMocks.module('grafana.services'));
+
+ beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
+ beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
+ ctx.$q = $q;
+ ctx.$rootScope = $rootScope;
+ ctx.$location = $location;
+ ctx.variableSrv = $injector.get('variableSrv');
+ ctx.variableSrv.init({
+ templating: {list: []},
+ events: new Emitter(),
+ });
+ ctx.$rootScope.$digest();
+ }));
+
+ function describeUpdateVariable(desc, fn) {
+ describe(desc, function() {
+ var scenario: any = {};
+ scenario.setup = function(setupFn) {
+ scenario.setupFn = setupFn;
+ };
+
+ beforeEach(function() {
+ scenario.setupFn();
+ var ds: any = {};
+ ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
+ ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
+ ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
+
+
+ scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel);
+ ctx.variableSrv.updateOptions(scenario.variable);
+ ctx.$rootScope.$digest();
+ });
+
+ fn(scenario);
+ });
+ }
+
+ describeUpdateVariable('interval variable without auto', scenario => {
+ scenario.setup(() => {
+ scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test'};
+ });
+
+ it('should update options array', () => {
+ expect(scenario.variable.options.length).to.be(4);
+ expect(scenario.variable.options[0].text).to.be('1s');
+ expect(scenario.variable.options[0].value).to.be('1s');
+ });
+ });
+
+ //
+ // Interval variable update
+ //
+ describeUpdateVariable('interval variable with auto', scenario => {
+ scenario.setup(() => {
+ scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
+
+ var range = {
+ from: moment(new Date()).subtract(7, 'days').toDate(),
+ to: new Date()
+ };
+
+ ctx.timeSrv.timeRange = sinon.stub().returns(range);
+ ctx.templateSrv.setGrafanaVariable = sinon.spy();
+ });
+
+ it('should update options array', function() {
+ expect(scenario.variable.options.length).to.be(5);
+ expect(scenario.variable.options[0].text).to.be('auto');
+ expect(scenario.variable.options[0].value).to.be('$__auto_interval');
+ });
+
+ it('should set $__auto_interval', function() {
+ var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
+ expect(call.args[0]).to.be('$__auto_interval');
+ expect(call.args[1]).to.be('12h');
+ });
+ });
+
+ //
+ // Query variable update
+ //
+ describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: '', name: 'test', current: {}};
+ scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
+ });
+
+ it('should set current value to first option', function() {
+ expect(scenario.variable.options.length).to.be(2);
+ expect(scenario.variable.current.value).to.be('backend1');
+ });
+ });
+
+ describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {
+ type: 'query',
+ query: '',
+ name: 'test',
+ current: {
+ value: ['val1', 'val2', 'val3'],
+ text: 'val1 + val2 + val3'
+ }
+ };
+ scenario.queryResult = [{text: 'val2'}, {text: 'val3'}];
+ });
+
+ it('should update current value', function() {
+ expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
+ expect(scenario.variable.current.text).to.eql('val2 + val3');
+ });
+ });
+
+ describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {
+ type: 'query',
+ query: '',
+ name: 'test',
+ current: {
+ value: ['val1', 'val2', 'val3'],
+ text: 'val1 + val2 + val3'
+ }
+ };
+ scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
+ });
+
+ it('should update current value with first one', function() {
+ expect(scenario.variable.current.value).to.eql('val5');
+ expect(scenario.variable.current.text).to.eql('val5');
+ });
+ });
+
+ describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {
+ type: 'query',
+ query: '',
+ name: 'test',
+ includeAll: true,
+ current: {
+ value: ['$__all'],
+ text: 'All'
+ }
+ };
+ scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
+ });
+
+ it('should keep current All value', function() {
+ expect(scenario.variable.current.value).to.eql(['$__all']);
+ expect(scenario.variable.current.text).to.eql('All');
+ });
+ });
+
+ describeUpdateVariable('query variable with numeric results', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = { type: 'query', query: '', name: 'test', current: {} };
+ scenario.queryResult = [{text: 12, value: 12}];
+ });
+
+ it('should set current value to first option', function() {
+ expect(scenario.variable.current.value).to.be('12');
+ expect(scenario.variable.options[0].value).to.be('12');
+ expect(scenario.variable.options[0].text).to.be('12');
+ });
+ });
+
+ describeUpdateVariable('basic query variable', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+ scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
+ });
+
+ it('should update options array', function() {
+ expect(scenario.variable.options.length).to.be(2);
+ expect(scenario.variable.options[0].text).to.be('backend1');
+ expect(scenario.variable.options[0].value).to.be('backend1');
+ expect(scenario.variable.options[1].value).to.be('backend2');
+ });
+
+ it('should select first option as value', function() {
+ expect(scenario.variable.current.value).to.be('backend1');
+ });
+ });
+
+ describeUpdateVariable('and existing value still exists in options', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
+ scenario.variableModel.current = { value: 'backend2', text: 'backend2'};
+ scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
+ });
+
+ it('should keep variable value', function() {
+ expect(scenario.variable.current.text).to.be('backend2');
+ });
+ });
+
+ describeUpdateVariable('and regex pattern exists', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
+ scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
+ scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
+ });
+
+ it('should extract and use match group', function() {
+ expect(scenario.variable.options[0].value).to.be('backend_01');
+ });
+ });
+
+ describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
+ scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
+ scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
+ });
+
+ it('should not add non matching items, None option should be added instead', function() {
+ expect(scenario.variable.options.length).to.be(1);
+ expect(scenario.variable.options[0].isNone).to.be(true);
+ });
+ });
+
+ describeUpdateVariable('regex pattern without slashes', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
+ scenario.variableModel.regex = 'backend_01';
+ scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
+ });
+
+ it('should return matches options', function() {
+ expect(scenario.variable.options.length).to.be(1);
+ });
+ });
+
+ describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
+ scenario.variableModel.regex = '/backend_01/';
+ scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}];
+ });
+
+ it('should return matches options', function() {
+ expect(scenario.variable.options.length).to.be(1);
+ });
+ });
+
+ describeUpdateVariable('with include All', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true};
+ scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
+ });
+
+ it('should add All option', function() {
+ expect(scenario.variable.options[0].text).to.be('All');
+ expect(scenario.variable.options[0].value).to.be('$__all');
+ });
+ });
+
+ describeUpdateVariable('with include all and custom value', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true, allValue: '*'};
+ scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
+ });
+
+ it('should add All option with custom value', function() {
+ expect(scenario.variable.options[0].value).to.be('$__all');
+ });
+ });
+
+ describeUpdateVariable('without sort', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 0};
+ scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
+ });
+
+ it('should return options without sort', function() {
+ expect(scenario.variable.options[0].text).to.be('bbb2');
+ expect(scenario.variable.options[1].text).to.be('aaa10');
+ expect(scenario.variable.options[2].text).to.be('ccc3');
+ });
+ });
+
+ describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 1};
+ scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
+ });
+
+ it('should return options with alphabetical sort', function() {
+ expect(scenario.variable.options[0].text).to.be('aaa10');
+ expect(scenario.variable.options[1].text).to.be('bbb2');
+ expect(scenario.variable.options[2].text).to.be('ccc3');
+ });
+ });
+
+ describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 2};
+ scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
+ });
+
+ it('should return options with alphabetical sort', function() {
+ expect(scenario.variable.options[0].text).to.be('ccc3');
+ expect(scenario.variable.options[1].text).to.be('bbb2');
+ expect(scenario.variable.options[2].text).to.be('aaa10');
+ });
+ });
+
+ describeUpdateVariable('with numerical sort (asc)', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 3};
+ scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
+ });
+
+ it('should return options with numerical sort', function() {
+ expect(scenario.variable.options[0].text).to.be('bbb2');
+ expect(scenario.variable.options[1].text).to.be('ccc3');
+ expect(scenario.variable.options[2].text).to.be('aaa10');
+ });
+ });
+
+ describeUpdateVariable('with numerical sort (desc)', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 4};
+ scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
+ });
+
+ it('should return options with numerical sort', function() {
+ expect(scenario.variable.options[0].text).to.be('aaa10');
+ expect(scenario.variable.options[1].text).to.be('ccc3');
+ expect(scenario.variable.options[2].text).to.be('bbb2');
+ });
+ });
+
+ //
+ // datasource variable update
+ //
+ describeUpdateVariable('datasource variable with regex filter', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {
+ type: 'datasource',
+ query: 'graphite',
+ name: 'test',
+ current: {value: 'backend4_pee', text: 'backend4_pee'},
+ regex: '/pee$/'
+ };
+ scenario.metricSources = [
+ {name: 'backend1', meta: {id: 'influx'}},
+ {name: 'backend2_pee', meta: {id: 'graphite'}},
+ {name: 'backend3', meta: {id: 'graphite'}},
+ {name: 'backend4_pee', meta: {id: 'graphite'}},
+ ];
+ });
+
+ it('should set only contain graphite ds and filtered using regex', function() {
+ expect(scenario.variable.options.length).to.be(2);
+ expect(scenario.variable.options[0].value).to.be('backend2_pee');
+ expect(scenario.variable.options[1].value).to.be('backend4_pee');
+ });
+
+ it('should keep current value if available', function() {
+ expect(scenario.variable.current.value).to.be('backend4_pee');
+ });
+ });
+
+ //
+ // Custom variable update
+ //
+ describeUpdateVariable('update custom variable', function(scenario) {
+ scenario.setup(function() {
+ scenario.variableModel = {type: 'custom', query: 'hej, hop, asd', name: 'test'};
+ });
+
+ it('should update options array', function() {
+ expect(scenario.variable.options.length).to.be(3);
+ expect(scenario.variable.options[0].text).to.be('hej');
+ expect(scenario.variable.options[1].value).to.be('hop');
+ });
+ });
+});
diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js
index b8d6cbaee2d..f7784e2cb50 100644
--- a/public/app/features/templating/templateSrv.js
+++ b/public/app/features/templating/templateSrv.js
@@ -1,10 +1,9 @@
define([
'angular',
'lodash',
- './editorCtrl',
- './templateValuesSrv',
+ 'app/core/utils/kbn',
],
-function (angular, _) {
+function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
@@ -16,6 +15,7 @@ function (angular, _) {
this._index = {};
this._texts = {};
this._grafanaVariables = {};
+ this._adhocVariables = {};
this.init = function(variables) {
this.variables = variables;
@@ -24,19 +24,32 @@ function (angular, _) {
this.updateTemplateData = function() {
this._index = {};
+ this._filters = {};
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
+
+ // add adhoc filters to it's own index
+ if (variable.type === 'adhoc') {
+ this._adhocVariables[variable.datasource] = variable;
+ continue;
+ }
+
if (!variable.current || !variable.current.isNone && !variable.current.value) {
continue;
}
+
this._index[variable.name] = variable;
}
};
- function regexEscape(value) {
- return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
- }
+ this.getAdhocFilters = function(datasourceName) {
+ var variable = this._adhocVariables[datasourceName];
+ if (variable) {
+ return variable.filters || [];
+ }
+ return [];
+ };
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
@@ -63,10 +76,10 @@ function (angular, _) {
switch(format) {
case "regex": {
if (typeof value === 'string') {
- return regexEscape(value);
+ return kbn.regexEscape(value);
}
- var escapedValues = _.map(value, regexEscape);
+ var escapedValues = _.map(value, kbn.regexEscape);
return '(' + escapedValues.join('|') + ')';
}
case "lucene": {
@@ -97,17 +110,6 @@ function (angular, _) {
return match && (self._index[match[1] || match[2]] !== void 0);
};
- this.containsVariable = function(str, variableName) {
- if (!str) {
- return false;
- }
-
- variableName = regexEscape(variableName);
- var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
- var match = findVarRegex.exec(str);
- return match !== null;
- };
-
this.highlightVariablesAsHtml = function(str) {
if (!str || !_.isString(str)) { return str; }
@@ -196,18 +198,11 @@ function (angular, _) {
this.fillVariableValuesForUrl = function(params, scopedVars) {
_.each(this.variables, function(variable) {
- var current = variable.current;
- var value = current.value;
-
- if (current.text === 'All') {
- value = 'All';
- }
-
if (scopedVars && scopedVars[variable.name] !== void 0) {
- value = scopedVars[variable.name].value;
+ params['var-' + variable.name] = scopedVars[variable.name].value;
+ } else {
+ params['var-' + variable.name] = variable.getValueForUrl();
}
-
- params['var-' + variable.name] = value;
});
};
diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js
index 3a7885ad5a0..a3db47fe3a9 100644
--- a/public/app/features/templating/templateValuesSrv.js
+++ b/public/app/features/templating/templateValuesSrv.js
@@ -166,8 +166,7 @@ function (angular, _, $, kbn) {
if (otherVariable === updatedVariable) {
return;
}
- if ((otherVariable.type === "datasource" &&
- templateSrv.containsVariable(otherVariable.regex, updatedVariable.name)) ||
+ if (templateSrv.containsVariable(otherVariable.regex, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
return self.updateOptions(otherVariable);
@@ -188,6 +187,12 @@ function (angular, _, $, kbn) {
return;
}
+ if (variable.type === 'adhoc') {
+ variable.current = {};
+ variable.options = [];
+ return;
+ }
+
// extract options in comma separated string
variable.options = _.map(variable.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
@@ -271,7 +276,7 @@ function (angular, _, $, kbn) {
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
- if (!variable.options.length) { return; }
+ if (!variable.options.length) { return $q.when(); }
return self.setVariableValue(variable, variable.options[0], false);
}
diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts
new file mode 100644
index 00000000000..3e12b65ec16
--- /dev/null
+++ b/public/app/features/templating/variable.ts
@@ -0,0 +1,40 @@
+///
+
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+
+export interface Variable {
+ setValue(option);
+ updateOptions();
+ dependsOn(variable);
+ setValueFromUrl(urlValue);
+ getValueForUrl();
+ getModel();
+}
+
+export var variableTypes = {};
+
+export function assignModelProperties(target, source, defaults) {
+ _.forEach(defaults, function(value, key) {
+ target[key] = source[key] === undefined ? value : source[key];
+ });
+}
+
+export function containsVariable(...args: any[]) {
+ var variableName = args[args.length-1];
+ var str = args[0] || '';
+
+ for (var i = 1; i < args.length-1; i++) {
+ str += args[i] || '';
+ }
+
+ variableName = kbn.regexEscape(variableName);
+ var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
+ var match = findVarRegex.exec(str);
+ return match !== null;
+}
+
+
+
+
+
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
new file mode 100644
index 00000000000..b7013d517f4
--- /dev/null
+++ b/public/app/features/templating/variable_srv.ts
@@ -0,0 +1,233 @@
+///
+
+import angular from 'angular';
+import _ from 'lodash';
+import coreModule from 'app/core/core_module';
+import {Variable, variableTypes} from './variable';
+
+export class VariableSrv {
+ dashboard: any;
+ variables: any;
+ variableLock: any;
+
+ /** @ngInject */
+ constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
+ // update time variant variables
+ $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope);
+ $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
+ }
+
+ init(dashboard) {
+ this.variableLock = {};
+ this.dashboard = dashboard;
+
+ // create working class models representing variables
+ this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
+ this.templateSrv.init(this.variables);
+
+ // register event to sync back to persisted model
+ this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
+
+ // init variables
+ for (let variable of this.variables) {
+ this.variableLock[variable.name] = this.$q.defer();
+ }
+
+ var queryParams = this.$location.search();
+ return this.$q.all(this.variables.map(variable => {
+ return this.processVariable(variable, queryParams);
+ }));
+ }
+
+ onDashboardRefresh() {
+ var promises = this.variables
+ .filter(variable => variable.refresh === 2)
+ .map(variable => {
+ var previousOptions = variable.options.slice();
+
+ return variable.updateOptions()
+ .then(this.variableUpdated.bind(this, variable))
+ .then(() => {
+ if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
+ this.$rootScope.$emit('template-variable-value-updated');
+ }
+ });
+ });
+
+ return this.$q.all(promises);
+ }
+
+ processVariable(variable, queryParams) {
+ var dependencies = [];
+ var lock = this.variableLock[variable.name];
+
+ for (let otherVariable of this.variables) {
+ if (variable.dependsOn(otherVariable)) {
+ dependencies.push(this.variableLock[otherVariable.name].promise);
+ }
+ }
+
+ return this.$q.all(dependencies).then(() => {
+ var urlValue = queryParams['var-' + variable.name];
+ if (urlValue !== void 0) {
+ return variable.setValueFromUrl(urlValue).then(lock.resolve);
+ }
+
+ if (variable.refresh === 1 || variable.refresh === 2) {
+ return variable.updateOptions().then(lock.resolve);
+ }
+
+ lock.resolve();
+ }).finally(() => {
+ delete this.variableLock[variable.name];
+ });
+ }
+
+ createVariableFromModel(model) {
+ var ctor = variableTypes[model.type].ctor;
+ if (!ctor) {
+ throw "Unable to find variable constructor for " + model.type;
+ }
+
+ var variable = this.$injector.instantiate(ctor, {model: model});
+ return variable;
+ }
+
+ addVariable(model) {
+ var variable = this.createVariableFromModel(model);
+ this.variables.push(this.createVariableFromModel(variable));
+ return variable;
+ }
+
+ syncToDashboardModel() {
+ this.dashboard.templating.list = this.variables.map(variable => {
+ return variable.getModel();
+ });
+ }
+
+ updateOptions(variable) {
+ return variable.updateOptions();
+ }
+
+ variableUpdated(variable) {
+ // if there is a variable lock ignore cascading update because we are in a boot up scenario
+ if (this.variableLock[variable.name]) {
+ return this.$q.when();
+ }
+
+ // cascade updates to variables that use this variable
+ var promises = _.map(this.variables, otherVariable => {
+ if (otherVariable === variable) {
+ return;
+ }
+
+ if (otherVariable.dependsOn(variable)) {
+ return this.updateOptions(otherVariable);
+ }
+ });
+
+ return this.$q.all(promises);
+ }
+
+ selectOptionsForCurrentValue(variable) {
+ var i, y, value, option;
+ var selected: any = [];
+
+ for (i = 0; i < variable.options.length; i++) {
+ option = variable.options[i];
+ option.selected = false;
+ if (_.isArray(variable.current.value)) {
+ for (y = 0; y < variable.current.value.length; y++) {
+ value = variable.current.value[y];
+ if (option.value === value) {
+ option.selected = true;
+ selected.push(option);
+ }
+ }
+ } else if (option.value === variable.current.value) {
+ option.selected = true;
+ selected.push(option);
+ }
+ }
+
+ return selected;
+ }
+
+ validateVariableSelectionState(variable) {
+ if (!variable.current) {
+ if (!variable.options.length) { return this.$q.when(); }
+ return variable.setValue(variable.options[0]);
+ }
+
+ if (_.isArray(variable.current.value)) {
+ var selected = this.selectOptionsForCurrentValue(variable);
+
+ // if none pick first
+ if (selected.length === 0) {
+ selected = variable.options[0];
+ } else {
+ selected = {
+ value: _.map(selected, function(val) {return val.value;}),
+ text: _.map(selected, function(val) {return val.text;}).join(' + '),
+ };
+ }
+
+ return variable.setValue(selected);
+ } else {
+ var currentOption = _.find(variable.options, {text: variable.current.text});
+ if (currentOption) {
+ return variable.setValue(currentOption);
+ } else {
+ if (!variable.options.length) { return Promise.resolve(); }
+ return variable.setValue(variable.options[0]);
+ }
+ }
+ }
+
+ setOptionFromUrl(variable, urlValue) {
+ var promise = this.$q.when();
+
+ if (variable.refresh) {
+ promise = variable.updateOptions();
+ }
+
+ return promise.then(() => {
+ var option = _.find(variable.options, op => {
+ return op.text === urlValue || op.value === urlValue;
+ });
+
+ option = option || {text: urlValue, value: urlValue};
+ return variable.setValue(option);
+ });
+ }
+
+ setOptionAsCurrent(variable, option) {
+ variable.current = _.cloneDeep(option);
+
+ if (_.isArray(variable.current.text)) {
+ variable.current.text = variable.current.text.join(' + ');
+ }
+
+ this.selectOptionsForCurrentValue(variable);
+ return this.variableUpdated(variable);
+ }
+
+ updateUrlParamsWithCurrentVariables() {
+ // update url
+ var params = this.$location.search();
+
+ // remove variable params
+ _.each(params, function(value, key) {
+ if (key.indexOf('var-') === 0) {
+ delete params[key];
+ }
+ });
+
+ // add new values
+ this.templateSrv.fillVariableValuesForUrl(params);
+ // update url
+ this.$location.search(params);
+ }
+}
+
+coreModule.service('variableSrv', VariableSrv);
diff --git a/public/app/partials/valueSelectDropdown.html b/public/app/partials/valueSelectDropdown.html
index a6799c80fd0..d1ebce44040 100644
--- a/public/app/partials/valueSelectDropdown.html
+++ b/public/app/partials/valueSelectDropdown.html
@@ -1,5 +1,5 @@
-
+
{{vm.linkText}}
@@ -10,7 +10,7 @@
-
+
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js
index d9fc6464491..4365c2e2596 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.js
+++ b/public/app/plugins/datasource/cloudwatch/datasource.js
@@ -23,6 +23,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
var queries = [];
options = angular.copy(options);
+ options.targets = this.expandTemplateVariable(options.targets, templateSrv);
_.each(options.targets, function(target) {
if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
return;
@@ -337,6 +338,37 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
});
}
+ this.getExpandedVariables = function(target, dimensionKey, variable) {
+ return _.chain(variable.options)
+ .filter(function(v) {
+ return v.selected;
+ })
+ .map(function(v) {
+ var t = angular.copy(target);
+ t.dimensions[dimensionKey] = v.value;
+ return t;
+ }).value();
+ };
+
+ this.expandTemplateVariable = function(targets, templateSrv) {
+ var self = this;
+ return _.chain(targets)
+ .map(function(target) {
+ var dimensionKey = _.findKey(target.dimensions, function(v) {
+ return templateSrv.variableExists(v);
+ });
+
+ if (dimensionKey) {
+ var variable = _.find(templateSrv.variables, function(variable) {
+ return templateSrv.containsVariable(target.dimensions[dimensionKey], variable.name);
+ });
+ return self.getExpandedVariables(target, dimensionKey, variable);
+ } else {
+ return [target];
+ }
+ }).flatten().value();
+ };
+
this.convertToCloudWatchTime = function(date, roundUp) {
if (_.isString(date)) {
date = dateMath.parse(date, roundUp);
diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
index 86e085b3f6f..0b9b3b53fb6 100644
--- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
+++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
@@ -98,6 +98,38 @@ describe('CloudWatchDatasource', function() {
});
ctx.$rootScope.$apply();
});
+
+ it('should generate the correct targets by expanding template variables', function() {
+ var templateSrv = {
+ variables: [
+ {
+ name: 'instance_id',
+ options: [
+ { value: 'i-23456789', selected: false },
+ { value: 'i-34567890', selected: true }
+ ]
+ }
+ ],
+ variableExists: function (e) { return true; },
+ containsVariable: function (str, variableName) { return str.indexOf('$' + variableName) !== -1; }
+ };
+
+ var targets = [
+ {
+ region: 'us-east-1',
+ namespace: 'AWS/EC2',
+ metricName: 'CPUUtilization',
+ dimensions: {
+ InstanceId: '$instance_id'
+ },
+ statistics: ['Average'],
+ period: 300
+ }
+ ];
+
+ var result = ctx.ds.expandTemplateVariable(targets, templateSrv);
+ expect(result[0].dimensions.InstanceId).to.be('i-34567890');
+ });
});
function describeMetricFindQuery(query, func) {
diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js
index 410cb8fb736..0889c078082 100644
--- a/public/app/plugins/datasource/elasticsearch/datasource.js
+++ b/public/app/plugins/datasource/elasticsearch/datasource.js
@@ -177,11 +177,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var target;
var sentTargets = [];
+ // add global adhoc filters to timeFilter
+ var adhocFilters = templateSrv.getAdhocFilters(this.name);
+
for (var i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (target.hide) {continue;}
- var queryObj = this.queryBuilder.build(target);
+ var queryObj = this.queryBuilder.build(target, adhocFilters);
var esQuery = angular.toJson(queryObj);
var luceneQuery = target.query || '*';
luceneQuery = templateSrv.replace(luceneQuery, options.scopedVars, 'lucene');
@@ -247,7 +250,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
// Hide meta-fields and check field type
if (key[0] !== '_' &&
(!query.type ||
- query.type && typeMap[subObj.type] === query.type)) {
+ query.type && typeMap[subObj.type] === query.type)) {
fields[fieldName] = {
text: fieldName,
@@ -288,6 +291,10 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
esQuery = header + '\n' + esQuery + '\n';
return this._post('_msearch?search_type=count', esQuery).then(function(res) {
+ if (!res.responses[0].aggregations) {
+ return [];
+ }
+
var buckets = res.responses[0].aggregations["1"].buckets;
return _.map(buckets, function(bucket) {
return {text: bucket.key, value: bucket.key};
@@ -310,6 +317,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
return this.getTerms(query);
}
};
+
+ this.getTagKeys = function() {
+ return this.getFields({});
+ };
+
+ this.getTagValues = function(options) {
+ return this.getTerms({field: options.key, query: '*'});
+ };
}
return {
diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js
index 9c8217102aa..d256c6d1438 100644
--- a/public/app/plugins/datasource/elasticsearch/query_builder.js
+++ b/public/app/plugins/datasource/elasticsearch/query_builder.js
@@ -98,7 +98,23 @@ function (queryDef) {
return query;
};
- ElasticQueryBuilder.prototype.build = function(target) {
+ ElasticQueryBuilder.prototype.addAdhocFilters = function(query, adhocFilters) {
+ if (!adhocFilters) {
+ return;
+ }
+
+ var i, filter, condition;
+ var must = query.query.filtered.filter.bool.must;
+
+ for (i = 0; i < adhocFilters.length; i++) {
+ filter = adhocFilters[i];
+ condition = {};
+ condition[filter.key] = filter.value;
+ must.push({"term": condition});
+ }
+ };
+
+ ElasticQueryBuilder.prototype.build = function(target, adhocFilters) {
// make sure query has defaults;
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
target.dsType = 'elasticsearch';
@@ -125,6 +141,8 @@ function (queryDef) {
}
};
+ this.addAdhocFilters(query, adhocFilters);
+
// handle document query
if (target.bucketAggs.length === 0) {
metric = target.metrics[0];
diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts
index bbd5711fd1f..d9174e73969 100644
--- a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts
+++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts
@@ -238,4 +238,16 @@ describe('ElasticQueryBuilder', function() {
expect(firstLevel.aggs["2"].derivative.buckets_path).to.be("3");
});
+ it('with adhoc filters', function() {
+ var query = builder.build({
+ metrics: [{type: 'Count', id: '0'}],
+ timeField: '@timestamp',
+ bucketAggs: [{type: 'date_histogram', field: '@timestamp', id: '3'}],
+ }, [
+ {key: 'key1', operator: '=', value: 'value1'}
+ ]);
+
+ expect(query.query.filtered.filter.bool.must[1].term["key1"]).to.be("value1");
+ });
+
});
diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts
index 08af9e71a91..76a66d18e3c 100644
--- a/public/app/plugins/datasource/influxdb/datasource.ts
+++ b/public/app/plugins/datasource/influxdb/datasource.ts
@@ -7,6 +7,8 @@ import * as dateMath from 'app/core/utils/datemath';
import InfluxSeries from './influx_series';
import InfluxQuery from './influx_query';
import ResponseParser from './response_parser';
+import InfluxQueryBuilder from './query_builder';
+
export default class InfluxDatasource {
type: string;
@@ -43,19 +45,23 @@ export default class InfluxDatasource {
query(options) {
var timeFilter = this.getTimeFilter(options);
+ var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {};
+ var targets = _.cloneDeep(options.targets);
var queryTargets = [];
+ var queryModel;
var i, y;
- var allQueries = _.map(options.targets, (target) => {
+ var allQueries = _.map(targets, target => {
if (target.hide) { return ""; }
queryTargets.push(target);
// build query
- var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars);
- var query = queryModel.render(true);
- query = query.replace(/\$interval/g, (target.interval || options.interval));
- return query;
+ scopedVars.interval = {value: target.interval || options.interval};
+
+ queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
+ return queryModel.render(true);
+
}).reduce((acc, current) => {
if (current !== "") {
acc += ";" + current;
@@ -63,11 +69,21 @@ export default class InfluxDatasource {
return acc;
});
+ if (allQueries === '') {
+ return this.$q.when({data: []});
+ }
+
+ // add global adhoc filters to timeFilter
+ var adhocFilters = this.templateSrv.getAdhocFilters(this.name);
+ if (adhocFilters.length > 0 ) {
+ timeFilter += ' AND ' + queryModel.renderAdhocFilters(adhocFilters);
+ }
+
// replace grafana variables
- allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
+ scopedVars.timeFilter = {value: timeFilter};
// replace templated variables
- allQueries = this.templateSrv.replace(allQueries, options.scopedVars);
+ allQueries = this.templateSrv.replace(allQueries, scopedVars);
return this._seriesQuery(allQueries).then((data): any => {
if (!data || !data.results) {
@@ -102,7 +118,7 @@ export default class InfluxDatasource {
}
}
- return { data: seriesList };
+ return {data: seriesList};
});
};
@@ -124,16 +140,23 @@ export default class InfluxDatasource {
};
metricFindQuery(query) {
- var interpolated;
- try {
- interpolated = this.templateSrv.replace(query, null, 'regex');
- } catch (err) {
- return this.$q.reject(err);
- }
+ var interpolated = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(interpolated)
.then(_.curry(this.responseParser.parse)(query));
- };
+ }
+
+ getTagKeys(options) {
+ var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
+ var query = queryBuilder.buildExploreQuery('TAG_KEYS');
+ return this.metricFindQuery(query);
+ }
+
+ getTagValues(options) {
+ var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
+ var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
+ return this.metricFindQuery(query);
+ }
_seriesQuery(query) {
if (!query) { return this.$q.when({results: []}); }
@@ -141,7 +164,6 @@ export default class InfluxDatasource {
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
}
-
serializeParams(params) {
if (!params) { return '';}
diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts
index 1def8e8f424..2f03f37a0a1 100644
--- a/public/app/plugins/datasource/influxdb/influx_query.ts
+++ b/public/app/plugins/datasource/influxdb/influx_query.ts
@@ -2,6 +2,7 @@
import _ from 'lodash';
import queryPart from './query_part';
+import kbn from 'app/core/utils/kbn';
export default class InfluxQuery {
target: any;
@@ -155,7 +156,7 @@ export default class InfluxQuery {
if (operator !== '>' && operator !== '<') {
value = "'" + value.replace(/\\/g, '\\\\') + "'";
}
- } else if (interpolate){
+ } else if (interpolate) {
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
}
@@ -181,12 +182,26 @@ export default class InfluxQuery {
return policy + measurement;
}
+ interpolateQueryStr(value, variable, defaultFormatFn) {
+ // if no multi or include all do not regexEscape
+ if (!variable.multi && !variable.includeAll) {
+ return value;
+ }
+
+ if (typeof value === 'string') {
+ return kbn.regexEscape(value);
+ }
+
+ var escapedValues = _.map(value, kbn.regexEscape);
+ return escapedValues.join('|');
+ };
+
render(interpolate?) {
var target = this.target;
if (target.rawQuery) {
if (interpolate) {
- return this.templateSrv.replace(target.query, this.scopedVars, 'regex');
+ return this.templateSrv.replace(target.query, this.scopedVars, this.interpolateQueryStr);
} else {
return target.query;
}
@@ -236,4 +251,11 @@ export default class InfluxQuery {
return query;
}
+
+ renderAdhocFilters(filters) {
+ var conditions = _.map(filters, (tag, index) => {
+ return this.renderTagCondition(tag, index, false);
+ });
+ return conditions.join(' ');
+ }
}
diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
index 557c626a7cc..52beed1d080 100644
--- a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
+++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
@@ -237,6 +237,19 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).to.be('math');
});
+ describe('when render adhoc filters', function() {
+ it('should generate correct query segment', function() {
+ var query = new InfluxQuery({measurement: 'cpu', }, templateSrv, {});
+
+ var queryText = query.renderAdhocFilters([
+ {key: 'key1', operator: '=', value: 'value1'},
+ {key: 'key2', operator: '!=', value: 'value2'},
+ ]);
+
+ expect(queryText).to.be('"key1" = \'value1\' AND "key2" != \'value2\'');
+ });
+ });
+
});
});
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 53ce91144a4..eedf9c1badd 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -40,7 +40,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return backendSrv.datasourceRequest(options);
};
- function regexEscape(value) {
+ function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
}
@@ -51,10 +51,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
}
if (typeof value === 'string') {
- return regexEscape(value);
+ return prometheusSpecialRegexEscape(value);
}
- var escapedValues = _.map(value, regexEscape);
+ var escapedValues = _.map(value, prometheusSpecialRegexEscape);
return escapedValues.join('|');
};
diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js
index 69154ffed65..2e7353b6048 100755
--- a/public/app/plugins/panel/graph/graph.js
+++ b/public/app/plugins/panel/graph/graph.js
@@ -354,7 +354,8 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
function parseThresholdExpr(expr) {
var match, operator, value, precision;
- match = expr.match(/\s*([<=>~]*)\W*(\d+(\.\d+)?)/);
+ expr = String(expr);
+ match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/);
if (match) {
operator = match[1];
value = parseFloat(match[2]);
diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts
index 10c81b7212d..2065bffb130 100644
--- a/public/app/plugins/panel/graph/specs/graph_specs.ts
+++ b/public/app/plugins/panel/graph/specs/graph_specs.ts
@@ -312,5 +312,52 @@ describe('grafanaGraph', function() {
expect(ctx.plotOptions.yaxes[0].max).to.be(0);
});
});
+ describe('and negative values used', function() {
+ ctx.setup(function(ctrl, data) {
+ ctrl.panel.yaxes[0].min = '-10';
+ ctrl.panel.yaxes[0].max = '-13.14';
+ data[0] = new TimeSeries({
+ datapoints: [[120,10],[160,20]],
+ alias: 'series1',
+ });
+ });
+
+ it('should set min and max to negative', function() {
+ expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
+ expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
+ });
+ });
+ });
+ graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
+ describe('and Y-Min is 0 and Y-Max is 100', function() {
+ ctx.setup(function(ctrl, data) {
+ ctrl.panel.yaxes[0].min = 0;
+ ctrl.panel.yaxes[0].max = 100;
+ data[0] = new TimeSeries({
+ datapoints: [[120,10],[160,20]],
+ alias: 'series1',
+ });
+ });
+
+ it('should set min to 0 and max to 100', function() {
+ expect(ctx.plotOptions.yaxes[0].min).to.be(0);
+ expect(ctx.plotOptions.yaxes[0].max).to.be(100);
+ });
+ });
+ describe('and Y-Min is -100 and Y-Max is -10.5', function() {
+ ctx.setup(function(ctrl, data) {
+ ctrl.panel.yaxes[0].min = -100;
+ ctrl.panel.yaxes[0].max = -10.5;
+ data[0] = new TimeSeries({
+ datapoints: [[120,10],[160,20]],
+ alias: 'series1',
+ });
+ });
+
+ it('should set min to -100 and max to -10.5', function() {
+ expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
+ expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
+ });
+ });
});
});
diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html
index c0c522abac9..e4e8446934a 100644
--- a/public/app/plugins/panel/singlestat/editor.html
+++ b/public/app/plugins/panel/singlestat/editor.html
@@ -1,207 +1,125 @@
+
diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html
index d5565aa7ba1..6e7281dd78c 100644
--- a/public/app/plugins/panel/table/editor.html
+++ b/public/app/plugins/panel/table/editor.html
@@ -1,173 +1,139 @@