diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb7ec258ea..16b11573fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example - [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles - [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses! +- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname. +- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details. **InfluxDB Breaking changes** - To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema diff --git a/src/app/controllers/dashboardCtrl.js b/src/app/controllers/dashboardCtrl.js index b9893b2dcda..f4d057e77e5 100644 --- a/src/app/controllers/dashboardCtrl.js +++ b/src/app/controllers/dashboardCtrl.js @@ -51,7 +51,7 @@ function (angular, $, config, _) { // init services timeSrv.init($scope.dashboard); - templateValuesSrv.init($scope.dashboard); + templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState); panelMoveSrv.init($scope.dashboard, $scope); $scope.checkFeatureToggles(); diff --git a/src/app/directives/graphiteSegment.js b/src/app/directives/graphiteSegment.js index 032ea99c39a..f0116bc8847 100644 --- a/src/app/directives/graphiteSegment.js +++ b/src/app/directives/graphiteSegment.js @@ -94,7 +94,7 @@ function (angular, app, _, $) { }; $input.attr('data-provide', 'typeahead'); - $input.typeahead({ source: $scope.source, minLength: 0, items: 100, updater: $scope.updater }); + $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater }); var typeahead = $input.data('typeahead'); typeahead.lookup = function () { diff --git a/src/app/services/dashboard/dashboardViewStateSrv.js b/src/app/services/dashboard/dashboardViewStateSrv.js index 64d217f3dec..2f85da5d27a 100644 --- a/src/app/services/dashboard/dashboardViewStateSrv.js +++ b/src/app/services/dashboard/dashboardViewStateSrv.js @@ -14,9 +14,12 @@ function (angular, _, $) { // like fullscreen panel & edit function DashboardViewState($scope) { var self = this; + self.state = {}; + self.panelScopes = []; + self.$scope = $scope; $scope.exitFullscreen = function() { - if (self.fullscreen) { + if (self.state.fullscreen) { self.update({ fullscreen: false }); } }; @@ -28,42 +31,48 @@ function (angular, _, $) { } }); - this.panelScopes = []; - this.$scope = $scope; - this.update(this.getQueryStringState(), true); } DashboardViewState.prototype.needsSync = function(urlState) { - if (urlState.fullscreen !== this.fullscreen) { return true; } - if (urlState.edit !== this.edit) { return true; } - if (urlState.panelId !== this.panelId) { return true; } - return false; + return _.isEqual(this.state, urlState) === false; }; DashboardViewState.prototype.getQueryStringState = function() { var queryParams = $location.search(); - return { + var urlState = { panelId: parseInt(queryParams.panelId) || null, fullscreen: queryParams.fullscreen ? true : false, - edit: queryParams.edit ? true : false + edit: queryParams.edit ? true : false, }; + + _.each(queryParams, function(value, key) { + if (key.indexOf('var-') !== 0) { return; } + urlState[key] = value; + }); + + return urlState; + }; + + DashboardViewState.prototype.serializeToUrl = function() { + var urlState = _.clone(this.state); + urlState.fullscreen = this.state.fullscreen ? true : null, + urlState.edit = this.state.edit ? true : null; + + return urlState; }; DashboardViewState.prototype.update = function(state, skipUrlSync) { - _.extend(this, state); + _.extend(this.state, state); + this.fullscreen = this.state.fullscreen; - if (!this.fullscreen) { - this.panelId = null; - this.edit = false; + if (!this.state.fullscreen) { + this.state.panelId = null; + this.state.edit = false; } if (!skipUrlSync) { - $location.search({ - fullscreen: this.fullscreen ? true : null, - panelId: this.panelId, - edit: this.edit ? true : null - }); + $location.search(this.serializeToUrl()); } this.syncState(); @@ -76,7 +85,7 @@ function (angular, _, $) { if (this.fullscreenPanel) { this.leaveFullscreen(false); } - var panelScope = this.getPanelScope(this.panelId); + var panelScope = this.getPanelScope(this.state.panelId); this.enterFullscreen(panelScope); return; } @@ -118,8 +127,8 @@ function (angular, _, $) { var fullscreenHeight = Math.floor(docHeight * 0.7); this.oldTimeRange = panelScope.range; - panelScope.height = this.edit ? editHeight : fullscreenHeight; - panelScope.editMode = this.edit; + panelScope.height = this.state.edit ? editHeight : fullscreenHeight; + panelScope.editMode = this.state.edit; this.fullscreenPanel = panelScope; $(window).scrollTop(0); @@ -135,7 +144,7 @@ function (angular, _, $) { var self = this; self.panelScopes.push(panelScope); - if (self.panelId === panelScope.panel.id) { + if (self.state.panelId === panelScope.panel.id) { self.enterFullscreen(panelScope); } diff --git a/src/app/services/templateSrv.js b/src/app/services/templateSrv.js index f8247d1c5cb..d63a9df34d4 100644 --- a/src/app/services/templateSrv.js +++ b/src/app/services/templateSrv.js @@ -7,7 +7,7 @@ function (angular, _) { var module = angular.module('grafana.services'); - module.service('templateSrv', function($q, $routeParams) { + module.service('templateSrv', function() { var self = this; this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g; @@ -19,17 +19,10 @@ function (angular, _) { this.updateTemplateData(true); }; - this.updateTemplateData = function(initial) { + this.updateTemplateData = function() { var data = {}; _.each(this.variables, function(variable) { - if (initial) { - var urlValue = $routeParams[ variable.name ]; - if (urlValue) { - variable.current = { text: urlValue, value: urlValue }; - } - } - if (!variable.current || !variable.current.value) { return; } @@ -50,6 +43,10 @@ function (angular, _) { return match && (self._templateData[match[1] || match[2]] !== void 0); }; + this.containsVariable = function(str, variableName) { + return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1; + }; + this.highlightVariablesAsHtml = function(str) { if (!str || !_.isString(str)) { return str; } diff --git a/src/app/services/templateValuesSrv.js b/src/app/services/templateValuesSrv.js index 511d5bfb495..cf2d766edc4 100644 --- a/src/app/services/templateValuesSrv.js +++ b/src/app/services/templateValuesSrv.js @@ -18,17 +18,24 @@ function (angular, _, kbn) { } }); - this.init = function(dashboard) { + this.init = function(dashboard, viewstate) { this.variables = dashboard.templating.list; + this.viewstate = viewstate; templateSrv.init(this.variables); for (var i = 0; i < this.variables.length; i++) { - var param = this.variables[i]; - if (param.refresh) { - this.updateOptions(param); + var variable = this.variables[i]; + var urlValue = viewstate.state['var-' + variable.name]; + if (urlValue !== void 0) { + var option = _.findWhere(variable.options, { text: urlValue }); + option = option || { text: urlValue, value: urlValue }; + this.setVariableValue(variable, option, true); } - else if (param.type === 'interval') { - this.updateAutoInterval(param); + else if (variable.refresh) { + this.updateOptions(variable); + } + else if (variable.type === 'interval') { + this.updateAutoInterval(variable); } } }; @@ -63,7 +70,7 @@ function (angular, _, kbn) { if (otherVariable === updatedVariable) { return; } - if (otherVariable.query.indexOf('[[' + updatedVariable.name + ']]') !== -1) { + if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) { return self.updateOptions(otherVariable); } }); @@ -92,7 +99,6 @@ function (angular, _, kbn) { var datasource = datasourceSrv.get(variable.datasource); return datasource.metricFindQuery(variable.query) .then(function (results) { - variable.options = self.metricNamesToVariableValues(variable, results); if (variable.includeAll) { @@ -102,7 +108,7 @@ function (angular, _, kbn) { // if parameter has current value // if it exists in options array keep value if (variable.current) { - var currentExists = _.findWhere(variable.options, { value: variable.current.value }); + var currentExists = _.findWhere(variable.options, { text: variable.current.text }); if (currentExists) { return self.setVariableValue(variable, variable.current, true); } diff --git a/src/css/less/overrides.less b/src/css/less/overrides.less index 4415c85d3c0..58e72ef43fa 100644 --- a/src/css/less/overrides.less +++ b/src/css/less/overrides.less @@ -516,6 +516,11 @@ div.flot-text { } } +// typeahead max height +.typeahead { + max-height: 300px; + overflow-y: auto; +} // Labels & Badges .label-tag { diff --git a/src/test/specs/dashboardViewStateSrv-specs.js b/src/test/specs/dashboardViewStateSrv-specs.js index 92e444d55d0..82ccfe51363 100644 --- a/src/test/specs/dashboardViewStateSrv-specs.js +++ b/src/test/specs/dashboardViewStateSrv-specs.js @@ -20,6 +20,7 @@ define([ viewState.update(updateState); expect(location.search()).to.eql(updateState); expect(viewState.fullscreen).to.be(true); + expect(viewState.state.fullscreen).to.be(true); }); }); @@ -29,6 +30,7 @@ define([ viewState.update({fullscreen: false}); expect(location.search()).to.eql({}); expect(viewState.fullscreen).to.be(false); + expect(viewState.state.fullscreen).to.be(false); }); }); diff --git a/src/test/specs/templateSrv-specs.js b/src/test/specs/templateSrv-specs.js index ac711dce66a..938d8bfe0a1 100644 --- a/src/test/specs/templateSrv-specs.js +++ b/src/test/specs/templateSrv-specs.js @@ -62,6 +62,24 @@ define([ }); + describe('when checking if a string contains a variable', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]); + _templateSrv.updateTemplateData(); + }); + + 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 with [[var]] syntax', function() { + var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test'); + expect(contains).to.be(true); + }); + + }); + describe('updateTemplateData with simple value', function() { beforeEach(function() { _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]); diff --git a/src/test/specs/templateValuesSrv-specs.js b/src/test/specs/templateValuesSrv-specs.js index c0674a5cd75..bc0de36e959 100644 --- a/src/test/specs/templateValuesSrv-specs.js +++ b/src/test/specs/templateValuesSrv-specs.js @@ -27,20 +27,6 @@ define([ }); }); - describe.only('should init values', function() { - var variables = [ - { name: 'test', current: { value: 'hej' }} - ]; - var dashboard = { templating: { list: variables } }; - - beforeEach(function() { - ctx.service.init(dashboard); - }); - - it('should update options array', function() { - }); - }); - function describeUpdateVariable(desc, fn) { describe(desc, function() { var scenario = {}; @@ -139,12 +125,12 @@ define([ describeUpdateVariable('and existing value still exists in options', function(scenario) { scenario.setup(function() { scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variable.current = { value: 'backend2'}; + scenario.variable.current = { text: 'backend2'}; scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; }); it('should keep variable value', function() { - expect(scenario.variable.current.value).to.be('backend2'); + expect(scenario.variable.current.text).to.be('backend2'); }); }); @@ -196,18 +182,6 @@ define([ }); }); - describeUpdateVariable('and existing value still exists in options', function(scenario) { - scenario.setup(function() { - scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variable.current = { value: 'backend2'}; - scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; - }); - - it('should keep variable value', function() { - expect(scenario.variable.current.value).to.be('backend2'); - }); - }); - describeUpdateVariable('with include All glob syntax', function(scenario) { scenario.setup(function() { scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' };