diff --git a/public/app/features/all.js b/public/app/features/all.js
index 43d9f33f190..cd7adb49de6 100644
--- a/public/app/features/all.js
+++ b/public/app/features/all.js
@@ -2,7 +2,7 @@ define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotations_srv',
- './templating/templateSrv',
+ './templating/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',
diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts
new file mode 100644
index 00000000000..208212c4034
--- /dev/null
+++ b/public/app/features/templating/all.ts
@@ -0,0 +1,13 @@
+import './templateSrv';
+import './templateValuesSrv';
+import './editorCtrl';
+
+import {VariableSrv} from './variable_srv';
+import {IntervalVariable} from './interval_variable';
+import {QueryVariable} from './query_variable';
+
+export {
+ VariableSrv,
+ IntervalVariable,
+ QueryVariable,
+}
diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts
new file mode 100644
index 00000000000..e7177982e07
--- /dev/null
+++ b/public/app/features/templating/interval_variable.ts
@@ -0,0 +1,48 @@
+///
+
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+import {Variable} from './variable';
+import {VariableSrv, variableConstructorMap} from './variable_srv';
+
+export class IntervalVariable implements Variable {
+ auto_count: number;
+ auto_min: number;
+ options: any;
+ auto: boolean;
+ query: string;
+
+ /** @ngInject */
+ constructor(private model, private timeSrv, private templateSrv) {
+ _.extend(this, model);
+ }
+
+ setValue(option) {
+ if (this.auto) {
+ this.updateAutoValue();
+ }
+ }
+
+ updateAutoValue() {
+ // add auto option if missing
+ if (this.options.length && this.options[0].text !== 'auto') {
+ this.options.unshift({ text: 'auto', value: '$__auto_interval' });
+ }
+
+ var interval = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
+ this.templateSrv.setGrafanaVariable('$__auto_interval', interval);
+ }
+
+ updateOptions() {
+ // extract options in comma separated string
+ this.options = _.map(this.query.split(/[,]+/), function(text) {
+ return {text: text.trim(), value: text.trim()};
+ });
+
+ if (this.auto) {
+ this.updateAutoValue();
+ }
+ }
+}
+
+variableConstructorMap['interval'] = IntervalVariable;
diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts
new file mode 100644
index 00000000000..2bcd89460d3
--- /dev/null
+++ b/public/app/features/templating/query_variable.ts
@@ -0,0 +1,125 @@
+///
+
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+import {Variable} from './variable';
+import {VariableSrv, variableConstructorMap} 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;
+ includeAll: boolean;
+
+ constructor(private model, private datasourceSrv, private templateSrv, private variableSrv) {
+ _.extend(this, model);
+ }
+
+ setValue(option){
+ this.current = _.cloneDeep(option);
+
+ if (_.isArray(this.current.text)) {
+ this.current.text = this.current.text.join(' + ');
+ }
+
+ this.variableSrv.selectOptionsForCurrentValue(this);
+ return this.variableSrv.variableUpdated(this);
+ }
+
+ updateOptions() {
+ return this.datasourceSrv.get(this.datasource)
+ .then(this.updateOptionsFromMetricFindQuery.bind(this))
+ .then(() => {
+ this.variableSrv.validateVariableSelectionState(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.model.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 = value;
+ }
+ }
+
+ options.push({text: text, value: value});
+ }
+
+ options = _.uniq(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(new RegExp(".*?(\d+).*"));
+ if (!matches) {
+ return 0;
+ } else {
+ return parseInt(matches[1], 10);
+ }
+ });
+ }
+ if (reverseSort) {
+ options = options.reverse();
+ }
+
+ return options;
+ }
+}
+
+variableConstructorMap['query'] = QueryVariable;
diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts
index 4d0c88e2e39..c5a1ea6f536 100644
--- a/public/app/features/templating/specs/variable_srv_specs.ts
+++ b/public/app/features/templating/specs/variable_srv_specs.ts
@@ -1,4 +1,279 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
-describe('VariableSrv', function() {
+import moment from 'moment';
+import helpers from 'test/specs/helpers';
+import '../all';
+
+describe.only('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.variableSrv = $injector.get('variableSrv');
+ ctx.variableSrv.init({templating: {list: []}});
+ 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));
+
+ 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');
+ });
+ });
+
});
+
+
diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js
index 130e2fed246..d6ed0b65877 100644
--- a/public/app/features/templating/templateSrv.js
+++ b/public/app/features/templating/templateSrv.js
@@ -1,9 +1,6 @@
define([
'angular',
'lodash',
- './editorCtrl',
- './variable_srv',
- './templateValuesSrv',
],
function (angular, _) {
'use strict';
diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts
new file mode 100644
index 00000000000..fb8b08112d6
--- /dev/null
+++ b/public/app/features/templating/variable.ts
@@ -0,0 +1,5 @@
+
+export interface Variable {
+ setValue(option);
+}
+
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
index d03559537dc..e7470bdfa98 100644
--- a/public/app/features/templating/variable_srv.ts
+++ b/public/app/features/templating/variable_srv.ts
@@ -3,49 +3,13 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
+import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
+import {IntervalVariable} from './interval_variable';
+import {Variable} from './variable';
-interface Variable {
-}
-
-class ConstantVariable implements Variable {
- constructor(private model) {
- }
-}
-
-class CustomVariable implements Variable {
- constructor(private model) {
- }
-}
-
-class IntervalVariable implements Variable {
- constructor(private model) {
- }
-}
-
-
-class QueryVariable implements Variable {
-
- constructor(private model,
- private variableSrv: VariableSrv,
- private datasourceSrv) {
- _.extend(this, model);
- }
-
- updateOptions() {
- return this.datasourceSrv.get(this.datasource)
- .then(_.partial(this.updateOptionsFromMetricFindQuery, variable))
- .then(_.partial(this.updateTags, variable))
- .then(_.partial(this.validateVariableSelectionState, variable));
- }
-}
-
-class DatasourceVariable implements Variable {
- constructor(private model) {
- }
-}
-
+export var variableConstructorMap: any = {};
export class VariableSrv {
dashboard: any;
@@ -55,53 +19,116 @@ export class VariableSrv {
/** @ngInject */
constructor(
- private $q,
private $rootScope,
- private datasourceSrv,
+ private $q,
private $location,
- private templateSrv,
- private timeSrv) {
+ private $injector,
+ private templateSrv) {
+ }
- }
+ init(dashboard) {
+ this.variableLock = {};
+ this.dashboard = dashboard;
+ this.variables = [];
- init(dashboard) {
- this.variableLock = {};
- this.dashboard = dashboard;
+ dashboard.templating.list.map(this.addVariable.bind(this));
+ this.templateSrv.init(this.variables);
- this.variables = dashboard.templating.list.map(item => {
- return new QueryVariable(item, this);
- });
-
- this.templateSrv.init(this.variables);
- return this.$q.when();
- }
-
- 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();
}
- var promises = _.map(this.variables, otherVariable => {
- if (otherVariable === variable) {
- return;
+ addVariable(model) {
+ var ctor = variableConstructorMap[model.type];
+ if (!ctor) {
+ throw "Unable to find variable constructor for " + model.type;
}
- if (this.templateSrv.containsVariable(otherVariable.regex, variable.name) ||
- this.templateSrv.containsVariable(otherVariable.query, variable.name) ||
- this.templateSrv.containsVariable(otherVariable.datasource, variable.name)) {
+ var variable = this.$injector.instantiate(ctor, {model: model});
+ this.variables.push(variable);
+ this.dashboard.templating.list.push(model);
+
+ return variable;
+ }
+
+ 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();
+ }
+
+ var promises = _.map(this.variables, otherVariable => {
+ if (otherVariable === variable) {
+ return;
+ }
+
+ if (this.templateSrv.containsVariable(otherVariable.regex, variable.name) ||
+ this.templateSrv.containsVariable(otherVariable.query, variable.name) ||
+ this.templateSrv.containsVariable(otherVariable.datasource, variable.name)) {
return this.updateOptions(otherVariable);
}
- });
+ });
- return this.$q.all(promises);
- }
+ 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 Promise.resolve(); }
+ 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]);
+ }
+ }
+ }
}
coreModule.service('variableSrv', VariableSrv);