Commit 46ebae73 by Torkel Ödegaard

feat(templating): progress on template system rewrite #6048

parent 7e8b2798
......@@ -10,7 +10,7 @@ function($, _, moment) {
kbn.valueFormats = {};
kbn.regexEscape = function(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&')
};
///// HELPER FUNCTIONS /////
......
......@@ -5,9 +5,13 @@ import './editorCtrl';
import {VariableSrv} from './variable_srv';
import {IntervalVariable} from './interval_variable';
import {QueryVariable} from './query_variable';
import {DatasourceVariable} from './datasource_variable';
import {CustomVariable} from './custom_variable';
export {
VariableSrv,
IntervalVariable,
QueryVariable,
DatasourceVariable,
CustomVariable,
}
///<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 CustomVariable implements Variable {
query: string;
options: any;
includeAll: boolean;
/** @ngInject */
constructor(private model, private timeSrv, private templateSrv) {
_.extend(this, model);
}
setValue(option) {
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
});
if (this.includeAll) {
this.addAllOption();
}
}
addAllOption() {
this.options.unshift({text: 'All', value: "$__all"});
}
dependsOn(variableName) {
return false;
}
}
variableConstructorMap['custom'] = CustomVariable;
///<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 DatasourceVariable implements Variable {
regex: any;
query: string;
options: any;
/** @ngInject */
constructor(private model, private datasourceSrv) {
_.extend(this, model);
}
setValue(option) {
}
updateOptions() {
var options = [];
var sources = this.datasourceSrv.getMetricSources({skipVariables: true});
var regex;
if (this.regex) {
regex = kbn.stringToJsRegex(this.regex);
}
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// must match on type
if (source.meta.id !== this.query) {
continue;
}
if (regex && !regex.exec(source.name)) {
continue;
}
options.push({text: source.name, value: source.name});
}
if (options.length === 0) {
options.push({text: 'No data sources found', value: ''});
}
this.options = options;
}
dependsOn(variableName) {
return false;
}
}
variableConstructorMap['datasource'] = DatasourceVariable;
......@@ -43,6 +43,10 @@ export class IntervalVariable implements Variable {
this.updateAutoValue();
}
}
dependsOn(variableName) {
return false;
}
}
variableConstructorMap['interval'] = IntervalVariable;
......@@ -2,7 +2,7 @@
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable} from './variable';
import {Variable, containsVariable} from './variable';
import {VariableSrv, variableConstructorMap} from './variable_srv';
function getNoneOption() {
......@@ -17,8 +17,9 @@ export class QueryVariable implements Variable {
options: any;
current: any;
includeAll: boolean;
refresh: number;
constructor(private model, private datasourceSrv, private templateSrv, private variableSrv) {
constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) {
_.extend(this, model);
}
......@@ -33,6 +34,23 @@ export class QueryVariable implements Variable {
return this.variableSrv.variableUpdated(this);
}
setValueFromUrl(urlValue) {
var promise = this.$q.when();
if (this.refresh) {
promise = this.updateOptions();
}
return promise.then(() => {
var option = _.find(this.options, op => {
return op.text === urlValue || op.value === urlValue;
});
option = option || { text: urlValue, value: urlValue };
return this.setValue(option);
});
}
updateOptions() {
return this.datasourceSrv.get(this.datasource)
.then(this.updateOptionsFromMetricFindQuery.bind(this))
......@@ -102,24 +120,30 @@ export class QueryVariable implements Variable {
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);
}
var matches = opt.text.match(/.*?(\d+).*/);
if (!matches) {
return 0;
} else {
return parseInt(matches[1], 10);
}
});
}
if (reverseSort) {
options = options.reverse();
}
return options;
}
dependsOn(variableName) {
return containsVariable(this.query, variableName) || containsVariable(this.datasource, variableName);
}
}
variableConstructorMap['query'] = QueryVariable;
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {containsVariable} 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);
});
});
});
......@@ -4,7 +4,7 @@ import moment from 'moment';
import helpers from 'test/specs/helpers';
import '../all';
describe.only('VariableSrv', function() {
describe('VariableSrv', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.core'));
......@@ -15,11 +15,58 @@ describe.only('VariableSrv', function() {
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: []}});
ctx.$rootScope.$digest();
}));
function describeInitSceneario(desc, fn) {
describe(desc, function() {
var scenario: any = {
urlParams: {},
setup: 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);
ctx.$location.search = sinon.stub().returns(scenario.urlParams);
ctx.dashboard = {templating: {list: scenario.variables}};
ctx.variableSrv.init(ctx.dashboard);
ctx.$rootScope.$digest();
scenario.variables = ctx.variableSrv.variables;
});
fn(scenario);
});
}
describeInitSceneario('when setting simple variable via url', scenario => {
scenario.setup(() => {
scenario.variables = [{
name: 'apps',
type: 'query',
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");
});
});
function describeUpdateVariable(desc, fn) {
describe(desc, function() {
var scenario: any = {};
......@@ -32,6 +79,8 @@ describe.only('VariableSrv', function() {
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);
......@@ -274,6 +323,114 @@ describe.only('VariableSrv', function() {
});
});
});
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');
});
});
});
define([
'angular',
'lodash',
'app/core/utils/kbn',
],
function (angular, _) {
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
......@@ -32,10 +33,6 @@ function (angular, _) {
}
};
function regexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
}
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
}
......@@ -61,10 +58,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": {
......@@ -95,17 +92,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; }
......
import kbn from 'app/core/utils/kbn';
export function containsVariable(str, variableName) {
if (!str) {
return false;
}
variableName = kbn.regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
}
export interface Variable {
setValue(option);
updateOptions();
dependsOn(variableName);
}
......@@ -34,7 +34,52 @@ export class VariableSrv {
dashboard.templating.list.map(this.addVariable.bind(this));
this.templateSrv.init(this.variables);
return this.$q.when();
var queryParams = this.$location.search();
for (let variable of this.variables) {
this.variableLock[variable.name] = this.$q.defer();
}
var promises = [];
for (let variable of this.variables) {
promises.push(this.processVariable(variable, queryParams));
}
return this.$q.all(this.variables.map(variable => {
return this.processVariable(variable, queryParams);
}));
}
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(() => {
// if (_.isEmpty(variable.current) && variable.options.length) {
// self.setVariableValue(variable, variable.options[0]);
// }
lock.resolve();
});
}
lock.resolve();
}).finally(() => {
delete this.variableLock[variable.name];
});
}
addVariable(model) {
......@@ -60,14 +105,13 @@ export class VariableSrv {
return this.$q.when();
}
// cascade updates to variables that use this variable
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)) {
if (otherVariable.dependsOn(variable)) {
return this.updateOptions(otherVariable);
}
});
......@@ -101,7 +145,7 @@ export class VariableSrv {
validateVariableSelectionState(variable) {
if (!variable.current) {
if (!variable.options.length) { return Promise.resolve(); }
if (!variable.options.length) { return this.$q.when(); }
return variable.setValue(variable.options[0]);
}
......@@ -129,6 +173,7 @@ export class VariableSrv {
}
}
}
}
coreModule.service('variableSrv', VariableSrv);
......@@ -3,7 +3,6 @@
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import * as dateMath from 'app/core/utils/datemath';
import PrometheusMetricFindQuery from './metric_find_query';
......@@ -41,6 +40,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return backendSrv.datasourceRequest(options);
};
function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
}
this.interpolateQueryExpr = function(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
......@@ -48,10 +51,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
}
if (typeof value === 'string') {
return kbn.regexEscape(value);
return prometheusSpecialRegexEscape(value);
}
var escapedValues = _.map(value, kbn.regexEscape);
var escapedValues = _.map(value, prometheusSpecialRegexEscape);
return escapedValues.join('|');
};
......
......@@ -177,38 +177,6 @@ define([
var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).to.be('this $google ok');
});
});
describe('when checking if a string contains a variable', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
});
it('should find it with $var syntax', function() {
var contains = _templateSrv.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 = _templateSrv.containsVariable('this.$ServerDomain.filters', 'Server');
expect(contains).to.be(false);
});
it('should find it with [[var]] syntax', function() {
var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = _templateSrv.containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = _templateSrv.containsVariable('$env', 'env');
expect(contains).to.be(true);
});
});
describe('updateTemplateData with simple value', function() {
......
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