Commit 7e8b2798 by Torkel Ödegaard

feat(templating): good progress on new variable update code, #6048

parent 0f4a9f1e
...@@ -2,7 +2,7 @@ define([ ...@@ -2,7 +2,7 @@ define([
'./panellinks/module', './panellinks/module',
'./dashlinks/module', './dashlinks/module',
'./annotations/annotations_srv', './annotations/annotations_srv',
'./templating/templateSrv', './templating/all',
'./dashboard/all', './dashboard/all',
'./playlist/all', './playlist/all',
'./snapshot/all', './snapshot/all',
......
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,
}
///<reference path="../../headers/common.d.ts" />
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;
///<reference path="../../headers/common.d.ts" />
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;
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; 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');
});
});
}); });
define([ define([
'angular', 'angular',
'lodash', 'lodash',
'./editorCtrl',
'./variable_srv',
'./templateValuesSrv',
], ],
function (angular, _) { function (angular, _) {
'use strict'; 'use strict';
......
export interface Variable {
setValue(option);
}
...@@ -3,49 +3,13 @@ ...@@ -3,49 +3,13 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import {IntervalVariable} from './interval_variable';
import {Variable} from './variable';
interface Variable { export var variableConstructorMap: any = {};
}
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 class VariableSrv { export class VariableSrv {
dashboard: any; dashboard: any;
...@@ -55,27 +19,37 @@ export class VariableSrv { ...@@ -55,27 +19,37 @@ export class VariableSrv {
/** @ngInject */ /** @ngInject */
constructor( constructor(
private $q,
private $rootScope, private $rootScope,
private datasourceSrv, private $q,
private $location, private $location,
private templateSrv, private $injector,
private timeSrv) { private templateSrv) {
} }
init(dashboard) { init(dashboard) {
this.variableLock = {}; this.variableLock = {};
this.dashboard = dashboard; this.dashboard = dashboard;
this.variables = [];
this.variables = dashboard.templating.list.map(item => { dashboard.templating.list.map(this.addVariable.bind(this));
return new QueryVariable(item, this);
});
this.templateSrv.init(this.variables); this.templateSrv.init(this.variables);
return this.$q.when(); return this.$q.when();
} }
addVariable(model) {
var ctor = variableConstructorMap[model.type];
if (!ctor) {
throw "Unable to find variable constructor for " + model.type;
}
var variable = this.$injector.instantiate(ctor, {model: model});
this.variables.push(variable);
this.dashboard.templating.list.push(model);
return variable;
}
updateOptions(variable) { updateOptions(variable) {
return variable.updateOptions(); return variable.updateOptions();
} }
...@@ -101,7 +75,60 @@ export class VariableSrv { ...@@ -101,7 +75,60 @@ export class VariableSrv {
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); coreModule.service('variableSrv', VariableSrv);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment