From 0a44add6c96cee16f71bf4afec50143b7e313783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Sep 2016 11:20:58 +0200 Subject: [PATCH 01/18] feat(adhoc fiters): began work on ad-hoc filters, refactored submenu filters to use gf-form styles, #6038 --- .../app/features/dashboard/submenu/submenu.html | 17 +++++++++++++---- public/app/features/templating/editorCtrl.js | 1 + .../features/templating/templateValuesSrv.js | 7 ++++++- public/app/partials/valueSelectDropdown.html | 4 ++-- public/sass/components/_submenu.scss | 5 +---- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 464d8c4cecf..f02b3cfc881 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -1,10 +1,19 @@ - @@ -215,26 +205,26 @@
-
- + + -
-
+ + + + +
-
Options
+
Options
Data source @@ -242,58 +232,58 @@
- + -
-
Selection Options
-
- - - - -
-
- Custom all value - -
-
+
+
Selection Options
+
+ + + + +
+
+ Custom all value + +
+
-
-
Value groups/tags (Experimental feature)
-
- -
-
- Tags query - -
-
-
  • Tag values query
  • - -
    -
    +
    +
    Value groups/tags (Experimental feature)
    +
    + +
    +
    + Tags query + +
    +
    +
  • Tag values query
  • + +
    +
    -
    -
    Preview of values (shows max 20)
    -
    -
    - {{option.text}} -
    -
    -
    - +
    +
    Preview of values (shows max 20)
    +
    +
    + {{option.text}} +
    +
    +
    + -
    - +
    +
    diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 82aaa2e5b8f..5d6492849f4 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -2,8 +2,8 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; -import {Variable, containsVariable, assignModelProperties} from './variable'; -import {VariableSrv, variableConstructorMap} from './variable_srv'; +import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable'; +import {VariableSrv} from './variable_srv'; function getNoneOption() { return { text: 'None', value: '', isNone: true }; @@ -37,8 +37,6 @@ export class QueryVariable implements Variable { current: {text: '', value: ''}, }; - supportsMulti = true; - constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) { // copy model properties to this instance assignModelProperties(this, model, this.defaults); @@ -151,4 +149,9 @@ export class QueryVariable implements Variable { } } -variableConstructorMap['query'] = QueryVariable; +variableTypes['query'] = { + name: 'Query', + ctor: QueryVariable, + description: 'Variable values are fetched from a datasource query', + supportsMulti: true, +}; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index b9441b55840..9a478b50840 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -11,6 +11,7 @@ export interface Variable { getModel(); } +export var variableTypes = {}; export function assignModelProperties(target, source, defaults) { _.forEach(defaults, function(value, key) { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index f31ce7515e0..d2efdeb971e 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -3,9 +3,7 @@ import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; -import {Variable} from './variable'; - -export var variableConstructorMap: any = {}; +import {Variable, variableTypes} from './variable'; export class VariableSrv { dashboard: any; @@ -85,7 +83,7 @@ export class VariableSrv { } createVariableFromModel(model) { - var ctor = variableConstructorMap[model.type]; + var ctor = variableTypes[model.type].ctor; if (!ctor) { throw "Unable to find variable constructor for " + model.type; } From cb522d58cd896b316994c56707215a83acc17c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 19 Sep 2016 18:41:42 +0200 Subject: [PATCH 15/18] feat(templating): back to be able to continue work on ad hoc filters, #6048 --- .../app/features/templating/adhoc_variable.ts | 52 +++++++++++++++++++ public/app/features/templating/all.ts | 2 + 2 files changed, 54 insertions(+) create mode 100644 public/app/features/templating/adhoc_variable.ts diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts new file mode 100644 index 00000000000..579b0268efa --- /dev/null +++ b/public/app/features/templating/adhoc_variable.ts @@ -0,0 +1,52 @@ +/// + +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import {Variable, assignModelProperties, variableTypes} from './variable'; +import {VariableSrv} from './variable_srv'; + +export class AdhocVariable implements Variable { + + defaults = { + type: 'adhoc', + name: '', + label: '', + hide: 0, + datasource: null, + options: [], + current: {}, + tags: {}, + }; + + /** @ngInject **/ + constructor(private model, private timeSrv, private templateSrv, private variableSrv) { + assignModelProperties(this, model, this.defaults); + } + + setValue(option) { + return Promise.resolve(); + } + + getModel() { + assignModelProperties(this.model, this, this.defaults); + return this.model; + } + + updateOptions() { + return Promise.resolve(); + } + + dependsOn(variable) { + return false; + } + + setValueFromUrl(urlValue) { + return Promise.resolve(); + } +} + +variableTypes['adhoc'] = { + name: 'Ad hoc', + ctor: AdhocVariable, + description: 'Ad hoc filters', +}; diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 3b36f18b9b0..7205da52d19 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -7,6 +7,7 @@ import {QueryVariable} from './query_variable'; import {DatasourceVariable} from './datasource_variable'; import {CustomVariable} from './custom_variable'; import {ConstantVariable} from './constant_variable'; +import {AdhocVariable} from './adhoc_variable'; export { VariableSrv, @@ -15,4 +16,5 @@ export { DatasourceVariable, CustomVariable, ConstantVariable, + AdhocVariable, } From 6e429b8575edc2a6ec7c27bb276534676da21bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 11:57:43 +0200 Subject: [PATCH 16/18] feat(adhoc filters): good progress on ad hoc filters and sync from to url, #6038 --- .../app/features/dashboard/ad_hoc_filters.ts | 36 ++- public/app/features/dashboard/viewStateSrv.js | 20 -- .../app/features/templating/adhoc_variable.ts | 28 ++- .../features/templating/constant_variable.ts | 7 + .../features/templating/custom_variable.ts | 11 +- .../templating/datasource_variable.ts | 7 +- .../features/templating/interval_variable.ts | 7 +- .../app/features/templating/query_variable.ts | 12 +- .../templating/specs/template_srv_specs.ts | 237 ++++++++++++++++++ public/app/features/templating/templateSrv.js | 13 +- public/app/features/templating/variable.ts | 1 + .../app/features/templating/variable_srv.ts | 18 ++ .../plugins/datasource/influxdb/datasource.ts | 6 +- public/test/specs/templateSrv-specs.js | 235 ----------------- 14 files changed, 351 insertions(+), 287 deletions(-) create mode 100644 public/app/features/templating/specs/template_srv_specs.ts delete mode 100644 public/test/specs/templateSrv-specs.js diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 426f6162467..f962f8ca2f4 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -21,7 +21,7 @@ export class AdHocFiltersCtrl { if (this.variable.value && !_.isArray(this.variable.value)) { } - for (let tag of this.variable.tags) { + for (let tag of this.variable.filters) { if (this.segments.length > 0) { this.segments.push(this.uiSegmentSrv.newCondition('AND')); } @@ -38,7 +38,11 @@ export class AdHocFiltersCtrl { getOptions(segment, index) { if (segment.type === 'operator') { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>', '=~', '!~'])); + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~'])); + } + + if (segment.type === 'condition') { + return this.$q.when([this.uiSegmentSrv.newSegment('AND')]); } return this.datasourceSrv.get(this.variable.datasource).then(ds => { @@ -100,37 +104,45 @@ export class AdHocFiltersCtrl { } updateVariableModel() { - var tags = []; - var tagIndex = -1; - var tagOperator = ""; + var filters = []; + var filterIndex = -1; + var operator = ""; + var hasFakes = false; - this.segments.forEach((segment, index) => { - if (segment.fake) { + this.segments.forEach(segment => { + if (segment.type === 'value' && segment.fake) { + hasFakes = true; return; } switch (segment.type) { case 'key': { - tags.push({key: segment.value}); - tagIndex += 1; + filters.push({key: segment.value}); + filterIndex += 1; break; } case 'value': { - tags[tagIndex].value = segment.value; + filters[filterIndex].value = segment.value; break; } case 'operator': { - tags[tagIndex].operator = segment.value; + filters[filterIndex].operator = segment.value; break; } case 'condition': { + filters[filterIndex].condition = segment.value; break; } } }); + if (hasFakes) { + return; + } + + this.variable.setFilters(filters); + this.$rootScope.$emit('template-variable-value-updated'); this.$rootScope.$broadcast('refresh'); - this.variable.tags = tags; } } diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 758d8ab7067..73f0c038c7f 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -34,10 +34,6 @@ function (angular, _, $) { $location.search(urlParams); }); - $scope.onAppEvent('template-variable-value-updated', function() { - self.updateUrlParamsWithCurrentVariables(); - }); - $scope.onAppEvent('$routeUpdate', function() { var urlState = self.getQueryStringState(); if (self.needsSync(urlState)) { @@ -57,22 +53,6 @@ function (angular, _, $) { this.expandRowForPanel(); } - DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() { - // update url - var params = $location.search(); - // remove variable params - _.each(params, function(value, key) { - if (key.indexOf('var-') === 0) { - delete params[key]; - } - }); - - // add new values - templateSrv.fillVariableValuesForUrl(params); - // update url - $location.search(params); - }; - DashboardViewState.prototype.expandRowForPanel = function() { if (!this.state.panelId) { return; } diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index 579b0268efa..ea942beaef3 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -6,6 +6,7 @@ import {Variable, assignModelProperties, variableTypes} from './variable'; import {VariableSrv} from './variable_srv'; export class AdhocVariable implements Variable { + filters: any[]; defaults = { type: 'adhoc', @@ -13,9 +14,7 @@ export class AdhocVariable implements Variable { label: '', hide: 0, datasource: null, - options: [], - current: {}, - tags: {}, + filters: [], }; /** @ngInject **/ @@ -41,8 +40,31 @@ export class AdhocVariable implements Variable { } setValueFromUrl(urlValue) { + if (!_.isArray(urlValue)) { + urlValue = [urlValue]; + } + + this.filters = urlValue.map(item => { + var values = item.split('|'); + return { + key: values[0], + operator: values[1], + value: values[2], + }; + }); + return Promise.resolve(); } + + getValueForUrl() { + return this.filters.map(filter => { + return filter.key + '|' + filter.operator + '|' + filter.value; + }); + } + + setFilters(filters: any[]) { + this.filters = filters; + } } variableTypes['adhoc'] = { diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index f0a659e0c20..9fe2acfdfb2 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -7,6 +7,7 @@ import {VariableSrv} from './variable_srv'; export class ConstantVariable implements Variable { query: string; options: any[]; + current: any; defaults = { type: 'constant', @@ -14,6 +15,7 @@ export class ConstantVariable implements Variable { hide: 2, label: '', query: '', + current: {}, }; /** @ngInject */ @@ -43,6 +45,11 @@ export class ConstantVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } + } variableTypes['constant'] = { diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index 77dfd57cae8..90ce08cf9e4 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -10,6 +10,7 @@ export class CustomVariable implements Variable { options: any; includeAll: boolean; multi: boolean; + current: any; defaults = { type: 'custom', @@ -17,10 +18,11 @@ export class CustomVariable implements Variable { label: '', hide: 0, options: [], - current: {text: '', value: ''}, + current: {}, query: '', includeAll: false, multi: false, + allValue: null, }; /** @ngInject **/ @@ -61,6 +63,13 @@ export class CustomVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + if (this.current.text === 'All') { + return 'All'; + } + return this.current.value; + } } variableTypes['custom'] = { diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index b23b5e04e42..96776c31163 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -9,13 +9,14 @@ export class DatasourceVariable implements Variable { regex: any; query: string; options: any; + current: any; defaults = { type: 'datasource', name: '', hide: 0, label: '', - current: {text: '', value: ''}, + current: {}, regex: '', options: [], query: '', @@ -73,6 +74,10 @@ export class DatasourceVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } } variableTypes['datasource'] = { diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index 94bb608e7e3..d53e44ae533 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -12,6 +12,7 @@ export class IntervalVariable implements Variable { auto: boolean; query: string; refresh: number; + current: any; defaults = { type: 'interval', @@ -20,7 +21,7 @@ export class IntervalVariable implements Variable { label: '', refresh: 2, options: [], - current: {text: '', value: ''}, + current: {}, query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d', auto: false, auto_min: '10s', @@ -75,6 +76,10 @@ export class IntervalVariable implements Variable { this.updateAutoValue(); return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } } variableTypes['interval'] = { diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 5d6492849f4..a9ac6aa02b9 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -33,8 +33,11 @@ export class QueryVariable implements Variable { name: '', multi: false, includeAll: false, + allValue: null, options: [], - current: {text: '', value: ''}, + current: {}, + tagsQuery: null, + tagValuesQuery: null, }; constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) { @@ -56,6 +59,13 @@ export class QueryVariable implements Variable { 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)) 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/templateSrv.js b/public/app/features/templating/templateSrv.js index 7615aa7da0f..bca814d29a4 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -180,18 +180,11 @@ function (angular, _, kbn) { 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/variable.ts b/public/app/features/templating/variable.ts index 9a478b50840..3e12b65ec16 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -8,6 +8,7 @@ export interface Variable { updateOptions(); dependsOn(variable); setValueFromUrl(urlValue); + getValueForUrl(); getModel(); } diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index d2efdeb971e..b7013d517f4 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -14,6 +14,7 @@ export class VariableSrv { 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) { @@ -210,6 +211,23 @@ export class VariableSrv { 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/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 4bb183474fb..81cffd18364 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -56,9 +56,9 @@ export default class InfluxDatasource { // apply add hoc filters for (let variable of this.templateSrv.variables) { if (variable.type === 'adhoc' && variable.datasource === this.name) { - for (let tag of variable.tags) { - if (tag.key !== undefined && tag.value !== undefined) { - target.tags.push({key: tag.key, value: tag.value, condition: 'AND'}); + for (let filter of variable.filters) { + if (filter.key !== undefined && filter.value !== undefined) { + target.tags.push({key: filter.key, value: filter.value, condition: filter.condition, operator: filter.operator}); } } } diff --git a/public/test/specs/templateSrv-specs.js b/public/test/specs/templateSrv-specs.js deleted file mode 100644 index 3dac01fbdf2..00000000000 --- a/public/test/specs/templateSrv-specs.js +++ /dev/null @@ -1,235 +0,0 @@ -define([ - '../mocks/dashboard-mock', - 'lodash', - 'app/features/templating/templateSrv' -], function(dashboardMock) { - 'use strict'; - - describe('templateSrv', function() { - var _templateSrv; - var _dashboard; - - beforeEach(module('grafana.services')); - beforeEach(module(function() { - _dashboard = dashboardMock.create(); - })); - - beforeEach(inject(function(templateSrv) { - _templateSrv = templateSrv; - })); - - describe('init', function() { - beforeEach(function() { - _templateSrv.init([{ 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() { - _templateSrv.init([{ 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() { - _templateSrv.init([{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() { - _templateSrv.init([{ - 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() { - _templateSrv.init([{ - 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() { - _templateSrv.init([{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() { - _templateSrv.init([{ 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() { - _templateSrv.init([{ 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() { - _templateSrv.init([{ 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() { - _templateSrv.init([{ 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() { - _templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]); - }); - - it('should set multiple url params', function() { - var params = {}; - _templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}}); - expect(params['var-test']).to.eql('val1'); - }); - }); - - describe('replaceWithText', function() { - beforeEach(function() { - _templateSrv.init([ - { name: 'server', current: { value: '{asd,asd2}', text: 'All' } }, - { 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'); - }); - }); - - }); - -}); From dfe0f911053705e394457bab2919f659eada7021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 16:29:48 +0200 Subject: [PATCH 17/18] feat(adhoc): adhoc filters progress --- .../app/features/templating/adhoc_variable.ts | 6 +-- .../templating/specs/adhoc_variable_specs.ts | 40 +++++++++++++++++++ public/app/features/templating/templateSrv.js | 18 +++++++++ .../plugins/datasource/influxdb/datasource.ts | 33 +++++++-------- .../datasource/influxdb/influx_query.ts | 7 ++++ .../influxdb/specs/influx_query_specs.ts | 13 ++++++ 6 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 public/app/features/templating/specs/adhoc_variable_specs.ts diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index ea942beaef3..5ebffb0b0e6 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -18,7 +18,7 @@ export class AdhocVariable implements Variable { }; /** @ngInject **/ - constructor(private model, private timeSrv, private templateSrv, private variableSrv) { + constructor(private model) { assignModelProperties(this, model, this.defaults); } @@ -68,7 +68,7 @@ export class AdhocVariable implements Variable { } variableTypes['adhoc'] = { - name: 'Ad hoc', + name: 'Ad hoc filters', ctor: AdhocVariable, - description: 'Ad hoc filters', + description: 'Add key/value filters on the fly', }; 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/templateSrv.js b/public/app/features/templating/templateSrv.js index bca814d29a4..e5ef5a0f6b7 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -15,6 +15,7 @@ function (angular, _, kbn) { this._index = {}; this._texts = {}; this._grafanaVariables = {}; + this._adhocVariables = {}; this.init = function(variables) { this.variables = variables; @@ -23,16 +24,33 @@ function (angular, _, kbn) { 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; } }; + this.getAdhocFilters = function(datasourceName) { + var variable = this._adhocVariables[datasourceName]; + if (variable) { + return variable.filters || []; + } + return [] + }; + function luceneEscape(value) { return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1"); } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 81cffd18364..dd35f42f02e 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -44,36 +44,23 @@ export default class InfluxDatasource { query(options) { var timeFilter = this.getTimeFilter(options); - var scopedVars = _.extend({}, options.scopedVars); + var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {}; var targets = _.cloneDeep(options.targets); var queryTargets = []; + var queryModel; var i, y; var allQueries = _.map(targets, target => { if (target.hide) { return ""; } - if (!target.rawQuery) { - // apply add hoc filters - for (let variable of this.templateSrv.variables) { - if (variable.type === 'adhoc' && variable.datasource === this.name) { - for (let filter of variable.filters) { - if (filter.key !== undefined && filter.value !== undefined) { - target.tags.push({key: filter.key, value: filter.value, condition: filter.condition, operator: filter.operator}); - } - } - } - } - } - queryTargets.push(target); // build query scopedVars.interval = {value: target.interval || options.interval}; - var queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); - var query = queryModel.render(true); + queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); + return queryModel.render(true); - return query; }).reduce((acc, current) => { if (current !== "") { acc += ";" + current; @@ -81,6 +68,16 @@ 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 scopedVars.timeFilter = {value: timeFilter}; @@ -120,7 +117,7 @@ export default class InfluxDatasource { } } - return { data: seriesList }; + return {data: seriesList}; }); }; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index fddb452767a..2f03f37a0a1 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -251,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\''); + }); + }); + }); }); From 15423e6e51313e071ac64df8a313f813e397c610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 17:34:38 +0200 Subject: [PATCH 18/18] feat(adhoc filters): initial base mvp for adhoc filters are donecloses #6038 --- public/app/features/templating/editor_ctrl.ts | 22 ++++++-- .../features/templating/partials/editor.html | 51 ++++++++++--------- .../plugins/datasource/influxdb/datasource.ts | 2 + 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 25ddec62cce..81bf2d4d71c 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -9,6 +9,7 @@ export class VariableEditorCtrl { /** @ngInject */ constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) { $scope.variableTypes = variableTypes; + $scope.ctrl = {}; $scope.refreshOptions = [ {value: 0, text: "Never"}, @@ -60,9 +61,8 @@ export class VariableEditorCtrl { }; $scope.isValid = function() { - if (!$scope.current.name) { - $scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']); - return false; + if (!$scope.ctrl.form.$valid) { + return; } if (!$scope.current.name.match(/^\w+$/)) { @@ -79,6 +79,18 @@ export class VariableEditorCtrl { return true; }; + $scope.validate = function() { + $scope.infoText = ''; + if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) { + $scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource'; + datasourceSrv.get($scope.current.datasource).then(ds => { + if (!ds.supportAdhocFilters) { + $scope.infoText = 'This datasource does not support adhoc filters yet.'; + } + }); + } + }; + $scope.runQuery = function() { return variableSrv.updateOptions($scope.current).then(null, function(err) { if (err.data && err.data.message) { err.message = err.data.message; } @@ -90,6 +102,7 @@ export class VariableEditorCtrl { $scope.current = variable; $scope.currentIsNew = false; $scope.mode = 'edit'; + $scope.validate(); }; $scope.duplicate = function(variable) { @@ -126,6 +139,8 @@ export class VariableEditorCtrl { if (oldIndex !== -1) { this.variables[oldIndex] = $scope.current; } + + $scope.validate(); }; $scope.removeVariable = function(variable) { @@ -133,7 +148,6 @@ export class VariableEditorCtrl { $scope.variables.splice(index, 1); $scope.updateSubmenuVisibility(); }; - } } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index c74245ae6be..e485072eed0 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -70,13 +70,13 @@ -
    +
    Variable
    Name - +
    @@ -102,15 +102,14 @@
    -
    -
    +
    Interval Options
    Values - +
    Auto option @@ -134,15 +133,15 @@
    -
    +
    Custom Options
    Values separated by comma - +
    -
    +
    Constant options
    Value @@ -150,14 +149,14 @@
    -
    +
    Query Options
    Data source
    - +
    @@ -174,7 +173,7 @@
    Query - +
    @@ -223,19 +222,18 @@
    -
    +
    Options
    -
    Data source
    - +
    -
    +
    -
    -
    Selection Options
    +
    +
    Selection Options
    -
    +
    Preview of values (shows max 20)
    @@ -280,12 +278,17 @@
    -
    -
    - - -
    -
    +
    + {{infoText}} +
    + +
    + + +
    + + +
    diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index dd35f42f02e..4c707dd59d1 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -21,6 +21,7 @@ export default class InfluxDatasource { interval: any; supportAnnotations: boolean; supportMetrics: boolean; + supportAdhocFilters: boolean; responseParser: any; /** @ngInject */ @@ -39,6 +40,7 @@ export default class InfluxDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; this.supportAnnotations = true; this.supportMetrics = true; + this.supportAdhocFilters = true; this.responseParser = new ResponseParser(); }