From 1b59fb5be9ae776b03c949dd9095191fe82ebca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 Mar 2015 16:13:25 -0400 Subject: [PATCH 01/31] POC for repeating panels based on template variable options --- src/app/features/dashboard/all.js | 1 + src/app/features/dashboard/dashboardCtrl.js | 3 ++ src/app/features/dashboard/dashboardSrv.js | 1 + .../features/dashboard/dynamicDashboardSrv.js | 48 +++++++++++++++++++ src/app/features/panel/panelSrv.js | 1 + src/app/partials/panelgeneral.html | 7 +++ 6 files changed, 61 insertions(+) create mode 100644 src/app/features/dashboard/dynamicDashboardSrv.js diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index e9edfe7fcdd..811f48cc464 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -14,5 +14,6 @@ define([ './unsavedChangesSrv', './directives/dashSearchView', './graphiteImportCtrl', + './dynamicDashboardSrv', './importCtrl', ], function () {}); diff --git a/src/app/features/dashboard/dashboardCtrl.js b/src/app/features/dashboard/dashboardCtrl.js index 8430ac18631..c194dcdb7ed 100644 --- a/src/app/features/dashboard/dashboardCtrl.js +++ b/src/app/features/dashboard/dashboardCtrl.js @@ -15,6 +15,7 @@ function (angular, $, config) { dashboardKeybindings, timeSrv, templateValuesSrv, + dynamicDashboardSrv, dashboardSrv, dashboardViewStateSrv, $timeout) { @@ -44,6 +45,8 @@ function (angular, $, config) { // template values service needs to initialize completely before // the rest of the dashboard can load templateValuesSrv.init(dashboard).then(function() { + dynamicDashboardSrv.init(dashboard); + $scope.dashboard = dashboard; $scope.dashboardViewState = dashboardViewStateSrv.create($scope); $scope.dashboardMeta = data.meta; diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 90ec885ed41..0eab4f867d9 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -126,6 +126,7 @@ function (angular, $, kbn, _, moment) { var currentRow = this.rows[rowIndex]; currentRow.panels.push(newPanel); + return newPanel; }; p.formatDate = function(date, format) { diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js new file mode 100644 index 00000000000..0c54b61419a --- /dev/null +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -0,0 +1,48 @@ +define([ + 'angular', + 'lodash', +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('dynamicDashboardSrv', function() { + + this.init = function(dashboard) { + this.handlePanelRepeats(dashboard); + }; + + this.handlePanelRepeats = function(dashboard) { + var i, j, row, panel; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.repeat) { + this.repeatPanel(panel, row, dashboard); + } + } + } + }; + + this.repeatPanel = function(panel, row, dashboard) { + var variables = dashboard.templating.list; + var variable = _.findWhere(variables, {name: panel.repeat.replace('$', '')}); + if (!variable) { + return; + } + + _.each(variable.options, function(option) { + var copy = dashboard.duplicatePanel(panel, row); + copy.repeat = null; + console.log('duplicatePanel'); + }); + }; + + + }); + +}); + + diff --git a/src/app/features/panel/panelSrv.js b/src/app/features/panel/panelSrv.js index ec3373c3426..4ffebe67f18 100644 --- a/src/app/features/panel/panelSrv.js +++ b/src/app/features/panel/panelSrv.js @@ -11,6 +11,7 @@ function (angular, _, config) { module.service('panelSrv', function($rootScope, $timeout, datasourceSrv, $q) { this.init = function($scope) { + if (!$scope.panel.span) { $scope.panel.span = 12; } $scope.inspector = {}; diff --git a/src/app/partials/panelgeneral.html b/src/app/partials/panelgeneral.html index 21fbf68c68f..75cb2517192 100644 --- a/src/app/partials/panelgeneral.html +++ b/src/app/partials/panelgeneral.html @@ -10,6 +10,13 @@
+ +
+
Templating options
+
+ + +
From 5de499c7f628903b069df69e83e0e693080f1498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Mar 2015 12:30:42 -0400 Subject: [PATCH 02/31] Working on panel repeat --- src/app/features/dashboard/dynamicDashboardSrv.js | 6 ++++++ src/app/features/panel/panelHelper.js | 1 + src/app/features/templating/templateSrv.js | 7 ++++++- src/app/plugins/datasource/graphite/datasource.js | 6 +++--- src/test/specs/templateSrv-specs.js | 12 ++++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index 0c54b61419a..446b7c96392 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -33,9 +33,15 @@ function (angular, _) { return; } + dashboard.scopedVars = { + panel: {} + }; + _.each(variable.options, function(option) { var copy = dashboard.duplicatePanel(panel, row); copy.repeat = null; + dashboard.scopedVars.panel[panel.id] = {}; + dashboard.scopedVars.panel[panel.id][variable.name] = option.value; console.log('duplicatePanel'); }); }; diff --git a/src/app/features/panel/panelHelper.js b/src/app/features/panel/panelHelper.js index c2842fb225e..2f90615fac6 100644 --- a/src/app/features/panel/panelHelper.js +++ b/src/app/features/panel/panelHelper.js @@ -8,6 +8,7 @@ function (angular, _, kbn, $) { 'use strict'; var module = angular.module('grafana.services'); + module.service('panelHelper', function(timeSrv) { this.updateTimeRange = function(scope) { diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index 8fc45097944..6e4a25f3580 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -63,13 +63,18 @@ function (angular, _) { }); }; - this.replace = function(target) { + this.replace = function(target, scopedVars) { if (!target) { return; } var value; this._regex.lastIndex = 0; return target.replace(this._regex, function(match, g1, g2) { + if (scopedVars) { + value = scopedVars[g1 || g2]; + if (value) { return value; } + } + value = self._values[g1 || g2]; if (!value) { return match; } diff --git a/src/app/plugins/datasource/graphite/datasource.js b/src/app/plugins/datasource/graphite/datasource.js index 37ce4356ee8..6e0ae3ebd97 100644 --- a/src/app/plugins/datasource/graphite/datasource.js +++ b/src/app/plugins/datasource/graphite/datasource.js @@ -36,7 +36,7 @@ function (angular, _, $, config, kbn, moment) { maxDataPoints: options.maxDataPoints, }; - var params = this.buildGraphiteParams(graphOptions); + var params = this.buildGraphiteParams(graphOptions, options.panelId); if (options.format === 'png') { return $q.when(this.url + '/render' + '?' + params.join('&')); @@ -231,7 +231,7 @@ function (angular, _, $, config, kbn, moment) { '#Y', '#Z' ]; - GraphiteDatasource.prototype.buildGraphiteParams = function(options) { + GraphiteDatasource.prototype.buildGraphiteParams = function(options, panelId) { var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; var clean_options = [], targets = {}; var target, targetValue, i; @@ -252,7 +252,7 @@ function (angular, _, $, config, kbn, moment) { continue; } - targetValue = templateSrv.replace(target.target); + targetValue = templateSrv.replace(target.target, panelId); targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat); targets[this._seriesRefLetters[i]] = targetValue; } diff --git a/src/test/specs/templateSrv-specs.js b/src/test/specs/templateSrv-specs.js index bb85b9a1c98..57e853d8a3f 100644 --- a/src/test/specs/templateSrv-specs.js +++ b/src/test/specs/templateSrv-specs.js @@ -29,6 +29,18 @@ define([ }); }); + 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': 'mupp'}); + expect(target).to.be('this.mupp.filters'); + }); + }); + + describe('can check if variable exists', function() { beforeEach(function() { _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); From 9f729900f2574e2a9387f169ab5c95882de57c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Mar 2015 13:33:58 -0400 Subject: [PATCH 03/31] work on scoped variable values --- .../features/dashboard/dynamicDashboardSrv.js | 19 ++++++++++--------- src/app/features/panel/panelHelper.js | 1 + src/app/features/panel/panelMenu.js | 2 +- src/app/features/templating/templateSrv.js | 9 +++++++-- src/app/filters/all.js | 4 ++-- .../plugins/datasource/graphite/datasource.js | 6 +++--- src/test/specs/templateSrv-specs.js | 7 ++++++- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index 446b7c96392..c8bfd74c895 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -33,15 +33,16 @@ function (angular, _) { return; } - dashboard.scopedVars = { - panel: {} - }; - - _.each(variable.options, function(option) { - var copy = dashboard.duplicatePanel(panel, row); - copy.repeat = null; - dashboard.scopedVars.panel[panel.id] = {}; - dashboard.scopedVars.panel[panel.id][variable.name] = option.value; + _.each(variable.options, function(option, index) { + if (index > 0) { + var copy = dashboard.duplicatePanel(panel, row); + copy.repeat = null; + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + } else { + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + } console.log('duplicatePanel'); }); }; diff --git a/src/app/features/panel/panelHelper.js b/src/app/features/panel/panelHelper.js index 2f90615fac6..3d164798d9b 100644 --- a/src/app/features/panel/panelHelper.js +++ b/src/app/features/panel/panelHelper.js @@ -68,6 +68,7 @@ function (angular, _, kbn, $) { targets: scope.panel.targets, format: scope.panel.renderer === 'png' ? 'png' : 'json', maxDataPoints: scope.resolution, + scopedVars: scope.panel.scopedVars, cacheTimeout: scope.panel.cacheTimeout }; diff --git a/src/app/features/panel/panelMenu.js b/src/app/features/panel/panelMenu.js index a529dd87b5c..99b5bcfe3b8 100644 --- a/src/app/features/panel/panelMenu.js +++ b/src/app/features/panel/panelMenu.js @@ -11,7 +11,7 @@ function (angular, $, _) { .directive('panelMenu', function($compile, linkSrv) { var linkTemplate = '' + - '{{panel.title | interpolateTemplateVars}}' + + '{{panel.title | interpolateTemplateVars:this}}' + '' + ' {{panelMeta.timeInfo}}' + ''; diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index 6e4a25f3580..09aed015386 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -72,7 +72,7 @@ function (angular, _) { return target.replace(this._regex, function(match, g1, g2) { if (scopedVars) { value = scopedVars[g1 || g2]; - if (value) { return value; } + if (value) { return value.value; } } value = self._values[g1 || g2]; @@ -82,7 +82,7 @@ function (angular, _) { }); }; - this.replaceWithText = function(target) { + this.replaceWithText = function(target, scopedVars) { if (!target) { return; } var value; @@ -90,6 +90,11 @@ function (angular, _) { this._regex.lastIndex = 0; return target.replace(this._regex, function(match, g1, g2) { + if (scopedVars) { + var option = scopedVars[g1 || g2]; + if (option) { return option.text; } + } + value = self._values[g1 || g2]; text = self._texts[g1 || g2]; if (!value) { return match; } diff --git a/src/app/filters/all.js b/src/app/filters/all.js index e75d70043d8..fd22061da66 100755 --- a/src/app/filters/all.js +++ b/src/app/filters/all.js @@ -56,8 +56,8 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen }); module.filter('interpolateTemplateVars', function(templateSrv) { - function interpolateTemplateVars(text) { - return templateSrv.replaceWithText(text); + function interpolateTemplateVars(text, scope) { + return templateSrv.replaceWithText(text, scope.panel.scopedVars); } interpolateTemplateVars.$stateful = true; diff --git a/src/app/plugins/datasource/graphite/datasource.js b/src/app/plugins/datasource/graphite/datasource.js index 6e0ae3ebd97..53eda28ea3b 100644 --- a/src/app/plugins/datasource/graphite/datasource.js +++ b/src/app/plugins/datasource/graphite/datasource.js @@ -36,7 +36,7 @@ function (angular, _, $, config, kbn, moment) { maxDataPoints: options.maxDataPoints, }; - var params = this.buildGraphiteParams(graphOptions, options.panelId); + var params = this.buildGraphiteParams(graphOptions, options.scopedVars); if (options.format === 'png') { return $q.when(this.url + '/render' + '?' + params.join('&')); @@ -231,7 +231,7 @@ function (angular, _, $, config, kbn, moment) { '#Y', '#Z' ]; - GraphiteDatasource.prototype.buildGraphiteParams = function(options, panelId) { + GraphiteDatasource.prototype.buildGraphiteParams = function(options, scopedVars) { var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; var clean_options = [], targets = {}; var target, targetValue, i; @@ -252,7 +252,7 @@ function (angular, _, $, config, kbn, moment) { continue; } - targetValue = templateSrv.replace(target.target, panelId); + targetValue = templateSrv.replace(target.target, scopedVars); targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat); targets[this._seriesRefLetters[i]] = targetValue; } diff --git a/src/test/specs/templateSrv-specs.js b/src/test/specs/templateSrv-specs.js index 57e853d8a3f..b39fca0ac1a 100644 --- a/src/test/specs/templateSrv-specs.js +++ b/src/test/specs/templateSrv-specs.js @@ -35,9 +35,14 @@ define([ }); it('should replace $test with scoped value', function() { - var target = _templateSrv.replace('this.$test.filters', {'test': 'mupp'}); + 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'); + }); }); From 741a1736a490804201786e7492aa4996742b4fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Mar 2015 16:04:08 -0400 Subject: [PATCH 04/31] can handle updates --- .../features/dashboard/dynamicDashboardSrv.js | 20 +++++++++++++++++++ src/app/features/dashboard/submenuCtrl.js | 7 +++++-- .../features/templating/templateValuesSrv.js | 17 +++++----------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index c8bfd74c895..8af85bc0aba 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -13,6 +13,25 @@ function (angular, _) { this.handlePanelRepeats(dashboard); }; + this.update = function(dashboard) { + this.removeLinkedPanels(dashboard); + this.handlePanelRepeats(dashboard); + }; + + this.removeLinkedPanels = function(dashboard) { + var i, j, row, panel; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.linked) { + row.panels = _.without(row.panels, panel); + j = j - 1; + } + } + } + }; + this.handlePanelRepeats = function(dashboard) { var i, j, row, panel; for (i = 0; i < dashboard.rows.length; i++) { @@ -37,6 +56,7 @@ function (angular, _) { if (index > 0) { var copy = dashboard.duplicatePanel(panel, row); copy.repeat = null; + copy.linked = true; copy.scopedVars = {}; copy.scopedVars[variable.name] = option; } else { diff --git a/src/app/features/dashboard/submenuCtrl.js b/src/app/features/dashboard/submenuCtrl.js index 456cc1b76c1..cca0c9c731a 100644 --- a/src/app/features/dashboard/submenuCtrl.js +++ b/src/app/features/dashboard/submenuCtrl.js @@ -7,7 +7,7 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv) { + module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv, dynamicDashboardSrv) { var _d = { enable: true }; @@ -27,7 +27,10 @@ function (angular, _) { }; $scope.setVariableValue = function(param, option) { - templateValuesSrv.setVariableValue(param, option); + templateValuesSrv.setVariableValue(param, option).then(function() { + dynamicDashboardSrv.update($scope.dashboard); + $rootScope.$broadcast('refresh'); + }); }; $scope.init(); diff --git a/src/app/features/templating/templateValuesSrv.js b/src/app/features/templating/templateValuesSrv.js index 73bcfb662bd..63a7a863272 100644 --- a/src/app/features/templating/templateValuesSrv.js +++ b/src/app/features/templating/templateValuesSrv.js @@ -32,7 +32,7 @@ function (angular, _, kbn) { var option = _.findWhere(variable.options, { text: urlValue }); option = option || { text: urlValue, value: urlValue }; - var promise = this.setVariableValue(variable, option, true); + var promise = this.setVariableValue(variable, option); this.updateAutoInterval(variable); promises.push(promise); @@ -60,17 +60,10 @@ function (angular, _, kbn) { templateSrv.setGrafanaVariable('$__auto_interval', interval); }; - this.setVariableValue = function(variable, option, recursive) { + this.setVariableValue = function(variable, option) { variable.current = option; - templateSrv.updateTemplateData(); - - return this.updateOptionsInChildVariables(variable) - .then(function() { - if (!recursive) { - $rootScope.$broadcast('refresh'); - } - }); + return this.updateOptionsInChildVariables(variable); }; this.updateOptionsInChildVariables = function(updatedVariable) { @@ -117,11 +110,11 @@ function (angular, _, kbn) { if (variable.current) { var currentOption = _.findWhere(variable.options, { text: variable.current.text }); if (currentOption) { - return self.setVariableValue(variable, currentOption, true); + return self.setVariableValue(variable, currentOption); } } - return self.setVariableValue(variable, variable.options[0], true); + return self.setVariableValue(variable, variable.options[0]); }); }); }; From d08144e73091a96296387f927a2d69f52adede5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 11:15:21 -0400 Subject: [PATCH 05/31] Began work on template multi select feature --- src/app/controllers/search.js | 18 --------- src/app/directives/all.js | 1 + src/app/directives/giveFocus.js | 26 ++++++++++++ src/app/directives/templateParamSelector.js | 40 +++++++++++++++++++ .../partials/variableValueSelect.html | 23 +++++++++++ src/app/partials/search.html | 2 +- src/app/partials/submenu.html | 24 +++++++---- src/css/less/submenu.less | 29 +++++++++++++- 8 files changed, 136 insertions(+), 27 deletions(-) create mode 100644 src/app/directives/giveFocus.js create mode 100644 src/app/features/dashboard/partials/variableValueSelect.html diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index e031559f3a8..00ad972e60a 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -136,24 +136,6 @@ function (angular, _, config) { }); - module.directive('xngFocus', function() { - return function(scope, element, attrs) { - element.click(function(e) { - e.stopPropagation(); - }); - - scope.$watch(attrs.xngFocus,function (newValue) { - if (!newValue) { - return; - } - setTimeout(function() { - element.focus(); - var pos = element.val().length * 2; - element[0].setSelectionRange(pos, pos); - }, 200); - },true); - }; - }); module.directive('tagColorFromName', function() { diff --git a/src/app/directives/all.js b/src/app/directives/all.js index 6190ef89099..3ef0b669d6c 100644 --- a/src/app/directives/all.js +++ b/src/app/directives/all.js @@ -16,4 +16,5 @@ define([ './grafanaVersionCheck', './dropdown.typeahead', './topnav', + './giveFocus', ], function () {}); diff --git a/src/app/directives/giveFocus.js b/src/app/directives/giveFocus.js new file mode 100644 index 00000000000..ac4a80e6197 --- /dev/null +++ b/src/app/directives/giveFocus.js @@ -0,0 +1,26 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('giveFocus', function() { + return function(scope, element, attrs) { + element.click(function(e) { + e.stopPropagation(); + }); + + scope.$watch(attrs.giveFocus, function (newValue) { + if (!newValue) { return; } + + setTimeout(function() { + element.focus(); + var pos = element.val().length * 2; + element[0].setSelectionRange(pos, pos); + }, 200); + },true); + }; + }); +}); diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index d9ad33512b5..a7ae67e363c 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -84,4 +84,44 @@ function (angular, app, _, $) { } }; }); + + angular + .module('grafana.directives') + .directive('variableValueSelect', function($compile, $window, $timeout) { + return { + scope: { + variable: "=", + }, + templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', + link: function(scope, elem) { + var bodyEl = angular.element($window.document.body); + + scope.show = function() { + scope.selectorOpen = true; + scope.giveFocus = 1; + + $timeout(function() { + bodyEl.on('click', scope.bodyOnClick); + }, 0, false); + }; + + scope.hide = function() { + scope.selectorOpen = false; + bodyEl.off('click', scope.bodyOnClick); + }; + + + scope.bodyOnClick = function(e) { + var dropdown = elem.find('.variable-value-dropdown'); + if (dropdown.has(e.target).length === 0) { + scope.$apply(scope.hide); + } + }; + + scope.$on('$destroy', function() { + }); + }, + }; + }); + }); diff --git a/src/app/features/dashboard/partials/variableValueSelect.html b/src/app/features/dashboard/partials/variableValueSelect.html new file mode 100644 index 00000000000..2a923f14ab6 --- /dev/null +++ b/src/app/features/dashboard/partials/variableValueSelect.html @@ -0,0 +1,23 @@ + + {{variable.name}}: {{variable.current.text}} + + +
+
+ + + +
+ +
+
+ + + +
+
+
+ + diff --git a/src/app/partials/search.html b/src/app/partials/search.html index 76400cd3eba..1a28ddc74a2 100644 --- a/src/app/partials/search.html +++ b/src/app/partials/search.html @@ -2,7 +2,7 @@
-
diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index 416fb47558b..c5e2762985e 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -3,17 +3,27 @@
    -
  • - VARIABLES: -
  • -
  • - - ${{variable.name}}: - + +
  • + + + + + + + + + + + + +
  • diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 74a2664d3ce..32820b9f051 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -5,7 +5,7 @@ } .submenu-controls { - margin: 10px 10px 0 10px; + margin: 20px 0px 15px 20px; } .annotation-disabled, .annotation-disabled a { @@ -18,3 +18,30 @@ } } +.variable-value-link { + font-size: 16px; + margin-right: 20px; +} + +.variable-value-dropdown { + position: absolute; + top: 30px; + width: 300px; + height: 400px; + background: @grafanaPanelBackground; + box-shadow: 0px 0px 55px 0px black; + border: 1px solid @grafanaTargetFuncBackground; + z-index: 1000; + padding: 10px; + + .variable-options-container { + height: 350px; + overflow: auto; + display: block; + line-height: 28px; + } + + .variable-option { + display: block; + } +} From 35888c814c94e82f27a2f239c4f890ee108bca01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Mar 2015 23:09:50 -0400 Subject: [PATCH 06/31] more work on multi select --- src/app/directives/templateParamSelector.js | 77 +++++++- .../partials/variableValueSelect.html | 7 +- src/app/features/dashboard/submenuCtrl.js | 6 +- src/app/features/templating/editorCtrl.js | 4 +- src/app/features/templating/templateSrv.js | 14 +- .../features/templating/templateValuesSrv.js | 5 + src/app/partials/submenu.html | 3 +- src/app/partials/templating_editor.html | 176 ++++++++++-------- src/css/less/forms.less | 5 + src/css/less/submenu.less | 6 +- src/test/specs/templateSrv-specs.js | 28 +++ 11 files changed, 238 insertions(+), 93 deletions(-) diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index a7ae67e363c..8a21f75ef54 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -91,25 +91,84 @@ function (angular, app, _, $) { return { scope: { variable: "=", + onUpdated: "&" }, templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', link: function(scope, elem) { var bodyEl = angular.element($window.document.body); + var variable = scope.variable; scope.show = function() { scope.selectorOpen = true; scope.giveFocus = 1; + scope.oldCurrentText = variable.current.text; + + var currentValues = variable.current.value; + + if (_.isString(currentValues)) { + currentValues = [currentValues]; + } + + scope.options = _.map(variable.options, function(option) { + var op = {text: option.text, value: option.value}; + if (_.indexOf(currentValues, option.value) >= 0) { + op.selected = true; + } + return op; + }); $timeout(function() { bodyEl.on('click', scope.bodyOnClick); }, 0, false); }; - scope.hide = function() { - scope.selectorOpen = false; - bodyEl.off('click', scope.bodyOnClick); + scope.optionSelected = function(option) { + if (!variable.multi) { + _.each(scope.options, function(other) { + if (option !== other) { + other.selected = false; + } + }); + } + + var selected = _.filter(scope.options, {selected: true}); + + if (selected.length === 0) { + // encode the first selected if no option is selected + scope.options[0].selected = true; + $timeout(function() { + scope.optionSelected(scope.options[0]); + }); + return; + } + + if (selected.length > 1) { + if (selected[0].text === 'All') { + selected = selected.slice(1, selected.length); + } + } + + variable.current = { + text: _.pluck(selected, 'text').join(', '), + value: _.pluck(selected, 'value'), + }; + + // only single value + if (variable.current.value.length === 1) { + variable.current.value = selected[0].value; + } + + scope.updateLinkText(); }; + scope.hide = function() { + scope.selectorOpen = false; + if (scope.oldCurrentText !== variable.current.text) { + scope.onUpdated(); + } + + bodyEl.off('click', scope.bodyOnClick); + }; scope.bodyOnClick = function(e) { var dropdown = elem.find('.variable-value-dropdown'); @@ -118,7 +177,17 @@ function (angular, app, _, $) { } }; - scope.$on('$destroy', function() { + scope.updateLinkText = function() { + scope.linkText = ""; + if (!variable.hideLabel) { + scope.linkText = (variable.label || variable.name) + ': '; + } + + scope.linkText += variable.current.text; + }; + + scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label'], function() { + scope.updateLinkText(); }); }, }; diff --git a/src/app/features/dashboard/partials/variableValueSelect.html b/src/app/features/dashboard/partials/variableValueSelect.html index 2a923f14ab6..cc9d3b08f1b 100644 --- a/src/app/features/dashboard/partials/variableValueSelect.html +++ b/src/app/features/dashboard/partials/variableValueSelect.html @@ -1,5 +1,6 @@ - {{variable.name}}: {{variable.current.text}} + {{linkText}} +
    @@ -11,9 +12,9 @@
    -
    - +
    diff --git a/src/app/features/dashboard/submenuCtrl.js b/src/app/features/dashboard/submenuCtrl.js index 456cc1b76c1..5099147fd53 100644 --- a/src/app/features/dashboard/submenuCtrl.js +++ b/src/app/features/dashboard/submenuCtrl.js @@ -26,8 +26,10 @@ function (angular, _) { $rootScope.$broadcast('refresh'); }; - $scope.setVariableValue = function(param, option) { - templateValuesSrv.setVariableValue(param, option); + $scope.variableUpdated = function(variable) { + templateValuesSrv.variableUpdated(variable).then(function() { + $rootScope.$broadcast('refresh'); + }); }; $scope.init(); diff --git a/src/app/features/templating/editorCtrl.js b/src/app/features/templating/editorCtrl.js index f17c383155d..f48452e4569 100644 --- a/src/app/features/templating/editorCtrl.js +++ b/src/app/features/templating/editorCtrl.js @@ -17,6 +17,8 @@ function (angular, _) { options: [], includeAll: false, allFormat: 'glob', + multi: false, + multiFormat: 'glob', }; $scope.init = function() { @@ -75,7 +77,7 @@ function (angular, _) { if ($scope.current.datasource === void 0) { $scope.current.datasource = null; $scope.current.type = 'query'; - $scope.current.allFormat = 'Glob'; + $scope.current.allFormat = 'glob'; } }; diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index 8fc45097944..008d0653cd5 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -29,11 +29,23 @@ function (angular, _) { _.each(this.variables, function(variable) { if (!variable.current || !variable.current.value) { return; } - this._values[variable.name] = variable.current.value; + this._values[variable.name] = this.renderVariableValue(variable); this._texts[variable.name] = variable.current.text; }, this); }; + this.renderVariableValue = function(variable) { + var value = variable.current.value; + if (_.isString(value)) { + return value; + } else { + if (variable.multiFormat === 'regex values') { + return '(' + value.join('|') + ')'; + } + return '{' + value.join(',') + '}'; + } + }; + this.setGrafanaVariable = function (name, value) { this._grafanaVariables[name] = value; }; diff --git a/src/app/features/templating/templateValuesSrv.js b/src/app/features/templating/templateValuesSrv.js index 73bcfb662bd..7d4830d960d 100644 --- a/src/app/features/templating/templateValuesSrv.js +++ b/src/app/features/templating/templateValuesSrv.js @@ -73,6 +73,11 @@ function (angular, _, kbn) { }); }; + this.variableUpdated = function(variable) { + templateSrv.updateTemplateData(); + return this.updateOptionsInChildVariables(variable); + }; + this.updateOptionsInChildVariables = function(updatedVariable) { var promises = _.map(self.variables, function(otherVariable) { if (otherVariable === updatedVariable) { diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index c5e2762985e..bdd75cb50b6 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -1,11 +1,10 @@
    diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 3fe5cfffd42..f49d56876f2 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -43,5 +43,21 @@ .variable-option { display: block; + .fa { + font-size: 130%; + position: relative; + top: 2px; + padding-right: 6px; + } + .fa-check-square-o { display: none; } + + &.selected { + .fa-square-o { + display: none; + } + .fa-check-square-o { + display: inline-block; + } + } } } From 86e9d2cf07b983415e287f12b32107aa9d6d62e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Mar 2015 13:33:52 -0400 Subject: [PATCH 09/31] more work on repeating row --- src/app/directives/templateParamSelector.js | 6 ++---- src/app/features/dashboard/dynamicDashboardSrv.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index df3b668496e..6cd2ce5a964 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -102,7 +102,6 @@ function (angular, app, _, $) { scope.selectorOpen = true; scope.giveFocus = 1; scope.oldCurrentText = variable.current.text; - var currentValues = variable.current.value; if (_.isString(currentValues)) { @@ -110,11 +109,10 @@ function (angular, app, _, $) { } scope.options = _.map(variable.options, function(option) { - var op = {text: option.text, value: option.value}; if (_.indexOf(currentValues, option.value) >= 0) { - op.selected = true; + option.selected = true; } - return op; + return option; }); $timeout(function() { diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index 8af85bc0aba..49070e8ce49 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -10,6 +10,7 @@ function (angular, _) { module.service('dynamicDashboardSrv', function() { this.init = function(dashboard) { + this.removeLinkedPanels(dashboard); this.handlePanelRepeats(dashboard); }; @@ -25,6 +26,7 @@ function (angular, _) { for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.linked) { + console.log('removing panel: ' + panel.id); row.panels = _.without(row.panels, panel); j = j - 1; } @@ -52,7 +54,14 @@ function (angular, _) { return; } - _.each(variable.options, function(option, index) { + var selected; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, function(option, index) { if (index > 0) { var copy = dashboard.duplicatePanel(panel, row); copy.repeat = null; From 0aab51a73fdeed60be9cf32c6da1d1a9350cc437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Mar 2015 15:06:23 -0400 Subject: [PATCH 10/31] Added row repeats --- .../features/dashboard/dynamicDashboardSrv.js | 70 +++++++++++++++++-- src/app/partials/roweditor.html | 24 +++++-- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index 49070e8ce49..d2ac44d10e4 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -10,13 +10,13 @@ function (angular, _) { module.service('dynamicDashboardSrv', function() { this.init = function(dashboard) { - this.removeLinkedPanels(dashboard); this.handlePanelRepeats(dashboard); + this.handleRowRepeats(dashboard); }; this.update = function(dashboard) { - this.removeLinkedPanels(dashboard); this.handlePanelRepeats(dashboard); + this.handleRowRepeats(dashboard); }; this.removeLinkedPanels = function(dashboard) { @@ -26,7 +26,6 @@ function (angular, _) { for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.linked) { - console.log('removing panel: ' + panel.id); row.panels = _.without(row.panels, panel); j = j - 1; } @@ -35,6 +34,8 @@ function (angular, _) { }; this.handlePanelRepeats = function(dashboard) { + this.removeLinkedPanels(dashboard); + var i, j, row, panel; for (i = 0; i < dashboard.rows.length; i++) { row = dashboard.rows[i]; @@ -47,6 +48,68 @@ function (angular, _) { } }; + this.removeLinkedRows = function(dashboard) { + var i, row; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + if (row.linked) { + dashboard.rows = _.without(dashboard.rows, row); + i = i - 1; + } + } + }; + + this.handleRowRepeats = function(dashboard) { + this.removeLinkedRows(dashboard); + var i, row; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + if (row.repeat) { + this.repeatRow(row, dashboard); + } + } + }; + + this.repeatRow = function(row, dashboard) { + console.log('repeat row'); + var variables = dashboard.templating.list; + var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); + if (!variable) { + return; + } + + var selected, copy, i, panel; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, function(option, index) { + if (index > 0) { + copy = angular.copy(row); + copy.repeat = null; + copy.linked = true; + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = row.panels[i]; + panel.id = dashboard.getNextPanelId(); + } + + dashboard.rows.push(copy); + } else { + copy = row; + } + + for (i = 0; i < copy.panels.length; i++) { + panel = row.panels[i]; + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + } + }); + }; + this.repeatPanel = function(panel, row, dashboard) { var variables = dashboard.templating.list; var variable = _.findWhere(variables, {name: panel.repeat.replace('$', '')}); @@ -76,7 +139,6 @@ function (angular, _) { }); }; - }); }); diff --git a/src/app/partials/roweditor.html b/src/app/partials/roweditor.html index 4c77a0bf814..8202052821c 100644 --- a/src/app/partials/roweditor.html +++ b/src/app/partials/roweditor.html @@ -17,15 +17,27 @@
    -
    - +
    +
    Row details
    +
    + +
    +
    + +
    + +
    -
    - + +
    +
    Templating options
    +
    + + +
    - -
    +
    From c658189c85ab187d8f18d59e2c16a62e2996cb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 21 Mar 2015 18:39:43 -0400 Subject: [PATCH 11/31] Updated --- src/app/features/dashboard/dynamicDashboardSrv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index d2ac44d10e4..f7956fd94a7 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -104,7 +104,7 @@ function (angular, _) { for (i = 0; i < copy.panels.length; i++) { panel = row.panels[i]; - panel.scopedVars = {}; + panel.scopedVars = panel.scopedVars || {}; panel.scopedVars[variable.name] = option; } }); From 3f97bd8212269d6c1c01126c91eb95fd0bd9ba1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 13:59:20 +0200 Subject: [PATCH 12/31] Added files removed in merge --- public/app/directives/giveFocus.js | 0 .../features/dashboard/dynamicDashboardSrv.js | 146 ++++++++++++++++++ .../partials/variableValueSelect.html | 23 +++ 3 files changed, 169 insertions(+) create mode 100644 public/app/directives/giveFocus.js create mode 100644 public/app/features/dashboard/dynamicDashboardSrv.js create mode 100644 public/app/features/dashboard/partials/variableValueSelect.html diff --git a/public/app/directives/giveFocus.js b/public/app/directives/giveFocus.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js new file mode 100644 index 00000000000..333fb5aed08 --- /dev/null +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -0,0 +1,146 @@ +define([ + 'angular', + 'lodash', +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('dynamicDashboardSrv', function() { + + this.init = function(dashboard) { + this.handlePanelRepeats(dashboard); + this.handleRowRepeats(dashboard); + }; + + this.update = function(dashboard) { + this.handlePanelRepeats(dashboard); + this.handleRowRepeats(dashboard); + }; + + this.removeLinkedPanels = function(dashboard) { + var i, j, row, panel; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.linked) { + row.panels = _.without(row.panels, panel); + j = j - 1; + } + } + } + }; + + this.handlePanelRepeats = function(dashboard) { + this.removeLinkedPanels(dashboard); + + var i, j, row, panel; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.repeat) { + this.repeatPanel(panel, row, dashboard); + } + } + } + }; + + this.removeLinkedRows = function(dashboard) { + var i, row; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + if (row.linked) { + dashboard.rows = _.without(dashboard.rows, row); + i = i - 1; + } + } + }; + + this.handleRowRepeats = function(dashboard) { + this.removeLinkedRows(dashboard); + var i, row; + for (i = 0; i < dashboard.rows.length; i++) { + row = dashboard.rows[i]; + if (row.repeat) { + this.repeatRow(row, dashboard); + } + } + }; + + this.repeatRow = function(row, dashboard) { + console.log('repeat row'); + var variables = dashboard.templating.list; + var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); + if (!variable) { + return; + } + + var selected, copy, i, panel; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, function(option, index) { + if (index > 0) { + copy = angular.copy(row); + copy.repeat = null; + copy.linked = true; + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = row.panels[i]; + panel.id = dashboard.getNextPanelId(); + } + + dashboard.rows.push(copy); + } else { + copy = row; + } + + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.scopedVars = panel.scopedVars || {}; + panel.scopedVars[variable.name] = option; + } + }); + + }; + + this.repeatPanel = function(panel, row, dashboard) { + var variables = dashboard.templating.list; + var variable = _.findWhere(variables, {name: panel.repeat.replace('$', '')}); + if (!variable) { + return; + } + + var selected; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, function(option, index) { + if (index > 0) { + var copy = dashboard.duplicatePanel(panel, row); + copy.repeat = null; + copy.linked = true; + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + } else { + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + } + console.log('duplicatePanel'); + }); + }; + + }); + +}); + diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html new file mode 100644 index 00000000000..1904aa598ee --- /dev/null +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -0,0 +1,23 @@ + + {{linkText}} + + + + + From 158b77d54e5d9c8ebaba4a36003ac3ad32e6095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 15:01:27 +0200 Subject: [PATCH 13/31] small update to panel repeats --- .../features/dashboard/dynamicDashboardSrv.js | 1 + public/app/partials/roweditor.html | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 333fb5aed08..286eb116e25 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -29,6 +29,7 @@ function (angular, _) { row.panels = _.without(row.panels, panel); j = j - 1; } + delete panel.scopedVars; } } }; diff --git a/public/app/partials/roweditor.html b/public/app/partials/roweditor.html index 243546392cc..f47351c8ffa 100644 --- a/public/app/partials/roweditor.html +++ b/public/app/partials/roweditor.html @@ -17,14 +17,25 @@
    -
    - +
    +
    Row details
    +
    + +
    +
    + +
    + +
    -
    - + +
    +
    Templating options
    +
    + + +
    - -
    From ca7aa294e0524143f7a71a03db9fbc65b3e0ba6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 17:20:32 +0200 Subject: [PATCH 14/31] Began wriing unit test for new panel repeat features, #1888 --- .../app/features/dashboard/dashboardCtrl.js | 1 + public/app/features/dashboard/dashboardSrv.js | 6 +- .../features/dashboard/dynamicDashboardSrv.js | 13 +++- .../dashboard/partials/dashboardTopNav.html | 2 +- .../features/dashboard/unsavedChangesSrv.js | 3 +- public/test/specs/dashboardSrv-specs.js | 9 +-- .../test/specs/dynamicDashboardSrv-specs.js | 65 +++++++++++++++++++ public/test/test-main.js | 1 + 8 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 public/test/specs/dynamicDashboardSrv-specs.js diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ba86f9f5dba..41453bd77e3 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -48,6 +48,7 @@ function (angular, $, config) { // the rest of the dashboard can load templateValuesSrv.init(dashboard).finally(function() { dynamicDashboardSrv.init(dashboard); + $scope.dashboard = dashboard; $scope.dashboardMeta = dashboard.meta; $scope.dashboardViewState = dashboardViewStateSrv.create($scope); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 02d8f51fafe..afe5e7964c6 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -10,7 +10,7 @@ function (angular, $, kbn, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function(contextSrv) { + module.factory('dashboardSrv', function() { function DashboardModel (data, meta) { if (!data) { @@ -59,10 +59,6 @@ function (angular, $, kbn, _, moment) { meta.canStar = meta.canStar === false ? false : true; meta.canDelete = meta.canDelete === false ? false : true; - if (contextSrv.hasRole('Viewer')) { - meta.canSave = false; - } - if (!this.editable) { meta.canEdit = false; meta.canDelete = false; diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 286eb116e25..8d099a9f6c6 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -8,8 +8,11 @@ function (angular, _) { var module = angular.module('grafana.services'); module.service('dynamicDashboardSrv', function() { + var self = this; this.init = function(dashboard) { + this.iteration = 0; + this.handlePanelRepeats(dashboard); this.handleRowRepeats(dashboard); }; @@ -112,6 +115,15 @@ function (angular, _) { }; + this.getRepeatPanel = function(sourcePanel, row) { + for (var i = 0; i < row.panels.length; i++) { + var panel = row.panels[i]; + if (panel.sourcePanel === sourcePanel) { + return panel; + } + } + }; + this.repeatPanel = function(panel, row, dashboard) { var variables = dashboard.templating.list; var variable = _.findWhere(variables, {name: panel.repeat.replace('$', '')}); @@ -137,7 +149,6 @@ function (angular, _) { panel.scopedVars = {}; panel.scopedVars[variable.name] = option; } - console.log('duplicatePanel'); }); }; diff --git a/public/app/features/dashboard/partials/dashboardTopNav.html b/public/app/features/dashboard/partials/dashboardTopNav.html index 6f85d6b4e02..89a6a185759 100644 --- a/public/app/features/dashboard/partials/dashboardTopNav.html +++ b/public/app/features/dashboard/partials/dashboardTopNav.html @@ -27,7 +27,7 @@
  • -
  • +
  • -
    diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 3fdc6faf14f..f58fbf5cfc6 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -4,62 +4,114 @@ define([ ], function() { 'use strict'; - describe('dynamicDashboardSrv', function() { - var _dynamicDashboardSrv; - var _dashboardSrv; + function dynamicDashScenario(desc, func) { - beforeEach(module('grafana.services')); + describe(desc, function() { + var ctx = {}; - beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { - _dynamicDashboardSrv = dynamicDashboardSrv; - _dashboardSrv = dashboardSrv; - })); + ctx.setup = function (setupFunc) { - describe('given dashboard with panel repeat', function() { - var model; + beforeEach(module('grafana.services')); - beforeEach(function() { - model = _dashboardSrv.create({ - rows: [ - { - panels: [{id: 2, repeat: '$apps'}] - } - ], - templating: { - list: [{ - name: 'apps', - current: { - text: 'se1, se2', - value: ['se1', 'se2'] - }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - ] - }] - } - }, {}); + beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { + ctx.dynamicDashboardSrv = dynamicDashboardSrv; + ctx.dashboardSrv = dashboardSrv; - _dynamicDashboardSrv.init(model); + var model = { + rows: [], + templating: { list: [] } + }; + + setupFunc(model); + ctx.dash = ctx.dashboardSrv.create(model); + ctx.dynamicDashboardSrv.init(ctx.dash); + ctx.rows = ctx.dash.rows; + + })); + + }; + + func(ctx); + + }); + } + + dynamicDashScenario('given dashboard with panel repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + panels: [{id: 2, repeat: '$apps'}] }); - - it('should repeat panel one time', function() { - expect(model.rows[0].panels.length).to.be(2); + dash.templating.list.push({ + name: 'apps', + current: { + text: 'se1, se2', + value: ['se1', 'se2'] + }, + options: [ + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + ] }); + }); - it('should mark panel repeated', function() { - expect(model.rows[0].panels[0].linked).to.be(undefined); - expect(model.rows[0].panels[0].repeat).to.be('$apps'); - expect(model.rows[0].panels[1].linked).to.be(true); - expect(model.rows[0].panels[1].repeat).to.be(null); - }); + it('should repeat panel one time', function() { + expect(ctx.rows[0].panels.length).to.be(2); + }); - it('should set scopedVars on panels', function() { - expect(model.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); - expect(model.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); - }); + it('should mark panel repeated', function() { + expect(ctx.rows[0].panels[0].linked).to.be(undefined); + expect(ctx.rows[0].panels[0].repeat).to.be('$apps'); + expect(ctx.rows[0].panels[1].linked).to.be(true); + expect(ctx.rows[0].panels[1].repeat).to.be(null); + }); + it('should set scopedVars on panels', function() { + expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); + expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); }); }); + + dynamicDashScenario('given dashboard with row repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + repeat: '$servers', + panels: [{id: 2}] + }); + dash.templating.list.push({ + name: 'servers', + current: { + text: 'se1, se2', + value: ['se1', 'se2'] + }, + options: [ + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + ] + }); + }); + + it('should repeat row one time', function() { + expect(ctx.rows.length).to.be(2); + }); + + it('should keep panel ids on first row', function() { + expect(ctx.rows[0].panels[0].id).to.be(2); + }); + + it('should mark second row as repeated', function() { + expect(ctx.rows[0].linked).to.be(undefined); + expect(ctx.rows[0].repeat).to.be('$servers'); + expect(ctx.rows[1].linked).to.be(true); + expect(ctx.rows[1].repeat).to.be(null); + }); + + it('should set scopedVars on row panels', function() { + expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); + expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); + }); + + }); + + }); From 48b25bc327108fd4d609e310e52a02ff929cfd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 06:40:23 +0200 Subject: [PATCH 16/31] Fix to graph panel, and width=0 bug --- public/app/panels/graph/graph.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index 795aee03c20..e63bd6536c3 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -112,7 +112,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { } if (elem.width() === 0) { - return; + return true; } } @@ -277,7 +277,6 @@ function (angular, $, kbn, moment, _, GraphTooltip) { if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { return true; } - return false; } function addTimeAxis(options) { From 32fe723da627a3614d03d38753e7f4a31f89beb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 08:44:48 +0200 Subject: [PATCH 17/31] More optimizations and unit tests for panel repeats, #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 46 +++++++++++++------ .../test/specs/dynamicDashboardSrv-specs.js | 37 +++++++++++---- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 82b40007174..3f053e4e9af 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -8,14 +8,20 @@ function (angular, _) { var module = angular.module('grafana.services'); module.service('dynamicDashboardSrv', function() { + var self = this; + this.init = function(dashboard) { - this.iteration = 0; + this.dashboard = dashboard; + this.iteration = new Date().getTime(); this.handlePanelRepeats(dashboard); this.handleRowRepeats(dashboard); }; this.update = function(dashboard) { + this.dashboard = dashboard; + this.iteration = this.iteration + 1; + this.handlePanelRepeats(dashboard); this.handleRowRepeats(dashboard); }; @@ -36,8 +42,6 @@ function (angular, _) { }; this.handlePanelRepeats = function(dashboard) { - this.removeLinkedPanels(dashboard); - var i, j, row, panel; for (i = 0; i < dashboard.rows.length; i++) { row = dashboard.rows[i]; @@ -46,6 +50,11 @@ function (angular, _) { if (panel.repeat) { this.repeatPanel(panel, row, dashboard); } + // clean up old left overs + else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + row.panels = _.without(row.panels, panel); + j = j - 1; + } } } }; @@ -111,13 +120,27 @@ function (angular, _) { }); }; - this.getRepeatPanel = function(sourcePanel, row) { + this.getPanelClone = function(sourcePanel, row, index) { + // if first clone return source + if (index === 0) { + return sourcePanel; + } + + // first try finding an existing clone to use for (var i = 0; i < row.panels.length; i++) { var panel = row.panels[i]; - if (panel.sourcePanel === sourcePanel) { + if (panel.repeatIteration !== this.iteration && + panel.repeatPanelId === sourcePanel.id) { + panel.repeatIteration = this.iteration; return panel; } } + + var clone = this.dashboard.duplicatePanel(sourcePanel, row); + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + return clone; }; this.repeatPanel = function(panel, row, dashboard) { @@ -135,16 +158,9 @@ function (angular, _) { } _.each(selected, function(option, index) { - if (index > 0) { - var copy = dashboard.duplicatePanel(panel, row); - copy.repeat = null; - copy.linked = true; - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; - } else { - panel.scopedVars = {}; - panel.scopedVars[variable.name] = option; - } + var copy = self.getPanelClone(panel, row, index); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; }); }; diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index f58fbf5cfc6..c4e2ff9204c 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -26,13 +26,10 @@ define([ ctx.dash = ctx.dashboardSrv.create(model); ctx.dynamicDashboardSrv.init(ctx.dash); ctx.rows = ctx.dash.rows; - })); - }; func(ctx); - }); } @@ -59,10 +56,8 @@ define([ }); it('should mark panel repeated', function() { - expect(ctx.rows[0].panels[0].linked).to.be(undefined); expect(ctx.rows[0].panels[0].repeat).to.be('$apps'); - expect(ctx.rows[0].panels[1].linked).to.be(true); - expect(ctx.rows[0].panels[1].repeat).to.be(null); + expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); }); it('should set scopedVars on panels', function() { @@ -70,6 +65,34 @@ define([ expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); }); + describe('After a second iteration', function() { + var repeatedPanelAfterIteration1; + + beforeEach(function() { + repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should have reused same panel instances', function() { + expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); + }); + + it('should have same panel count', function() { + expect(ctx.rows[0].panels.length).to.be(2); + }); + }); + + describe('After a second iteration and selected values reduced', function() { + beforeEach(function() { + ctx.dash.templating.list[0].options[1].selected = false; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should clean up repeated panel', function() { + expect(ctx.rows[0].panels.length).to.be(1); + }); + }); + }); dynamicDashScenario('given dashboard with row repeat', function(ctx) { @@ -110,8 +133,6 @@ define([ expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); }); - }); - }); From 9590f485f1951b0fa0101b4a38979d5745988309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 09:24:07 +0200 Subject: [PATCH 18/31] More optimizations and unit tests for panel repeats, this time reuse rows, #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 83 +++++++++---------- .../test/specs/dynamicDashboardSrv-specs.js | 29 ++++++- 2 files changed, 67 insertions(+), 45 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 3f053e4e9af..391a8d1bf7b 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -26,21 +26,6 @@ function (angular, _) { this.handleRowRepeats(dashboard); }; - this.removeLinkedPanels = function(dashboard) { - var i, j, row, panel; - for (i = 0; i < dashboard.rows.length; i++) { - row = dashboard.rows[i]; - for (j = 0; j < row.panels.length; j++) { - panel = row.panels[j]; - if (panel.linked) { - row.panels = _.without(row.panels, panel); - j = j - 1; - } - delete panel.scopedVars; - } - } - }; - this.handlePanelRepeats = function(dashboard) { var i, j, row, panel; for (i = 0; i < dashboard.rows.length; i++) { @@ -59,28 +44,53 @@ function (angular, _) { } }; - this.removeLinkedRows = function(dashboard) { - var i, row; - for (i = 0; i < dashboard.rows.length; i++) { - row = dashboard.rows[i]; - if (row.linked) { - dashboard.rows.splice(i, 1); - i = i - 1; - } - } - }; - this.handleRowRepeats = function(dashboard) { - this.removeLinkedRows(dashboard); var i, row; for (i = 0; i < dashboard.rows.length; i++) { row = dashboard.rows[i]; if (row.repeat) { this.repeatRow(row, dashboard); } + // clean up old left overs + else if (row.repeatRowId && row.repeatIteration !== this.iteration) { + dashboard.rows.splice(i, 1); + i = i - 1; + } } }; + this.getRowClone = function(sourceRow, index) { + if (index === 0) { + return sourceRow; + } + + var i, panel, row; + var sourceRowId = _.indexOf(this.dashboard.rows, sourceRow) + 1; + + // look for row to reuse + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; + if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { + row.repeatIteration = this.iteration; + return row; + } + } + + var copy = angular.copy(sourceRow); + copy.repeat = null; + copy.repeatRowId = sourceRowId; + copy.repeatIteration = this.iteration; + this.dashboard.rows.push(copy); + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.id = this.dashboard.getNextPanelId(); + } + + return copy; + }; + this.repeatRow = function(row, dashboard) { var variables = dashboard.templating.list; var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); @@ -96,21 +106,7 @@ function (angular, _) { } _.each(selected, function(option, index) { - if (index > 0) { - copy = angular.copy(row); - copy.repeat = null; - copy.linked = true; - - dashboard.rows.push(copy); - - // set new panel ids - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.id = dashboard.getNextPanelId(); - } - } else { - copy = row; - } + copy = self.getRowClone(row, index); for (i = 0; i < copy.panels.length; i++) { panel = copy.panels[i]; @@ -129,8 +125,7 @@ function (angular, _) { // first try finding an existing clone to use for (var i = 0; i < row.panels.length; i++) { var panel = row.panels[i]; - if (panel.repeatIteration !== this.iteration && - panel.repeatPanelId === sourcePanel.id) { + if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { panel.repeatIteration = this.iteration; return panel; } diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index c4e2ff9204c..cba4d6f00ca 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -133,6 +133,33 @@ define([ expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); }); - }); + describe('After a second iteration', function() { + var repeatedRowAfterFirstIteration; + + beforeEach(function() { + repeatedRowAfterFirstIteration = ctx.rows[1]; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should still only have 2 rows', function() { + expect(ctx.rows.length).to.be(2); + }); + + it('should reuse row instance', function() { + expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); + }); + }); + + describe('After a second iteration and selected values reduced', function() { + beforeEach(function() { + ctx.dash.templating.list[0].options[1].selected = false; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should remove repeated second row', function() { + expect(ctx.rows.length).to.be(1); + }); + }); + }); }); From bcb80eb38f824616915cae7184dda8595ed1d77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 09:26:20 +0200 Subject: [PATCH 19/31] more tweaks --- public/app/panels/graph/module.js | 1 - public/test/specs/dynamicDashboardSrv-specs.js | 9 +++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/panels/graph/module.js b/public/app/panels/graph/module.js index 228fab59e03..dfcd5901594 100644 --- a/public/app/panels/graph/module.js +++ b/public/app/panels/graph/module.js @@ -24,7 +24,6 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }); module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper, $q) { - console.log('Graph: init: ' + $scope.panel.id); $scope.panelMeta = new PanelMeta({ panelName: 'Graph', diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index cba4d6f00ca..62c722cf98e 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -123,12 +123,17 @@ define([ }); it('should mark second row as repeated', function() { - expect(ctx.rows[0].linked).to.be(undefined); expect(ctx.rows[0].repeat).to.be('$servers'); - expect(ctx.rows[1].linked).to.be(true); + }); + + it('should clear repeat field on repeated row', function() { expect(ctx.rows[1].repeat).to.be(null); }); + it('should generate a repeartRowId based on repeat row index', function() { + expect(ctx.rows[1].repeatRowId).to.be(1); + }); + it('should set scopedVars on row panels', function() { expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); From 5768f107695509db96da52e8b4468ff892ddcf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 10:23:35 +0200 Subject: [PATCH 20/31] More optimizations and unit tests for panel repeats #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 51 ++++++++++++------- .../test/specs/dynamicDashboardSrv-specs.js | 10 ++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 391a8d1bf7b..bea79a81d8c 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -64,30 +64,35 @@ function (angular, _) { return sourceRow; } - var i, panel, row; + var i, panel, row, copy; var sourceRowId = _.indexOf(this.dashboard.rows, sourceRow) + 1; // look for row to reuse for (i = 0; i < this.dashboard.rows.length; i++) { row = this.dashboard.rows[i]; if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { - row.repeatIteration = this.iteration; - return row; + copy = row; + break; } } - var copy = angular.copy(sourceRow); + if (!copy) { + copy = angular.copy(sourceRow); + this.dashboard.rows.push(copy); + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.id = this.dashboard.getNextPanelId(); + } + + } else { + // update reused instance + } + copy.repeat = null; copy.repeatRowId = sourceRowId; copy.repeatIteration = this.iteration; - this.dashboard.rows.push(copy); - - // set new panel ids - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.id = this.dashboard.getNextPanelId(); - } - return copy; }; @@ -122,16 +127,28 @@ function (angular, _) { return sourcePanel; } + var i, tmpId, panel, clone; + // first try finding an existing clone to use - for (var i = 0; i < row.panels.length; i++) { - var panel = row.panels[i]; + for (i = 0; i < row.panels.length; i++) { + panel = row.panels[i]; if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { - panel.repeatIteration = this.iteration; - return panel; + clone = panel; + break; } } - var clone = this.dashboard.duplicatePanel(sourcePanel, row); + if (!clone) { + clone = { id: this.dashboard.getNextPanelId() }; + row.panels.push(clone); + } + + // save id + tmpId = clone.id; + // copy properties from source + angular.extend(clone, sourcePanel); + // restore id + clone.id = tmpId; clone.repeatIteration = this.iteration; clone.repeatPanelId = sourcePanel.id; clone.repeat = null; diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 62c722cf98e..968a181e288 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -70,6 +70,7 @@ define([ beforeEach(function() { repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; + ctx.rows[0].panels[0].fill = 10; ctx.dynamicDashboardSrv.update(ctx.dash); }); @@ -77,6 +78,10 @@ define([ expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); }); + it('reused panel should copy properties from source', function() { + expect(ctx.rows[0].panels[1].fill).to.be(10); + }); + it('should have same panel count', function() { expect(ctx.rows[0].panels.length).to.be(2); }); @@ -144,6 +149,7 @@ define([ beforeEach(function() { repeatedRowAfterFirstIteration = ctx.rows[1]; + ctx.rows[0].height = 500; ctx.dynamicDashboardSrv.update(ctx.dash); }); @@ -151,6 +157,10 @@ define([ expect(ctx.rows.length).to.be(2); }); + it.skip('should have updated props from source', function() { + expect(ctx.rows[1].height).to.be(500); + }); + it('should reuse row instance', function() { expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); }); From f6a61c1ec5229104661ac9b05a62e338bd9cc416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 12:02:39 +0200 Subject: [PATCH 21/31] Changes to unsaved changes service to ignore repeated panels and rows, #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 3 --- .../features/dashboard/unsavedChangesSrv.js | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index bea79a81d8c..2ecd526f795 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -85,9 +85,6 @@ function (angular, _) { panel = copy.panels[i]; panel.id = this.dashboard.getNextPanelId(); } - - } else { - // update reused instance } copy.repeat = null; diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index 3fd942f1139..97fa3027cfc 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -81,6 +81,26 @@ function(angular, _, config) { }); }; + this.cleanDashboardFromRepeatedPanelsAndRows = function(dash) { + dash.rows = _.filter(dash.rows, function(row) { + if (row.repeatRowId) { + console.log('filtering out row'); + return false; + } + + row.panels = _.filter(row.panels, function(panel) { + if (panel.repeatPanelId) { + return false; + } + // remove scopedVars + panel.scopedVars = null; + return true; + }); + + return true; + }); + }; + this.has_unsaved_changes = function() { if (!self.original) { return false; @@ -106,6 +126,9 @@ function(angular, _, config) { } }); + this.cleanDashboardFromRepeatedPanelsAndRows(current); + this.cleanDashboardFromRepeatedPanelsAndRows(original); + // ignore some panel and row stuff current.forEachPanel(function(panel, panelIndex, row, rowIndex) { var originalRow = original.rows[rowIndex]; From aaea80e0537301b1856c8ba09b2e083b829bb4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 13:45:59 +0200 Subject: [PATCH 22/31] A lot of refactoring opf unsaved changes service so it can be unit tested better --- ' | 47 +++++ .../app/features/dashboard/dashboardCtrl.js | 2 + .../features/dashboard/unsavedChangesSrv.js | 165 ++++++++---------- public/test/specs/unsavedChangesSrv-specs.js | 48 +++++ public/test/test-main.js | 1 + 5 files changed, 175 insertions(+), 88 deletions(-) create mode 100644 ' create mode 100644 public/test/specs/unsavedChangesSrv-specs.js diff --git a/' b/' new file mode 100644 index 00000000000..2b78b586722 --- /dev/null +++ b/' @@ -0,0 +1,47 @@ +define([ + 'features/dashboard/unsavedChangesSrv', + 'features/dashboard/dashboardSrv' +], function() { + 'use strict'; + + describe("unsavedChangesSrv", function() { + var _unsavedChangesSrv; + var _dashboardSrv; + var _location; + var _contextSrvStub = { + isEditor: true + }; + var _rootScope; + var tracker; + + beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', _contextSrvStub); + })); + + beforeEach(inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { + _unsavedChangesSrv = unsavedChangesSrv; + _dashboardSrv = dashboardSrv; + _location = $location; + _rootScope = $rootScope; + })); + + describe('when dashboard is modified and route changes', function() { + + beforeEach(function() { + var dash = _dashboardSrv.create({}); + var scope = _rootScope.$new(); + scope.appEvent = sinon.spy(); + scope.onAppEvent = sinon.spy(); + tracker = _unsavedChangesSrv.constructor(dash, scope); + }); + + it('No changes should not have changes', function() { + expect(tracker.hasChanges()).to.be(false); + }); + + }); + + }); + +}); diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index 41453bd77e3..f81b808a0e1 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -17,6 +17,7 @@ function (angular, $, config) { templateValuesSrv, dynamicDashboardSrv, dashboardSrv, + unsavedChangesSrv, dashboardViewStateSrv, contextSrv, $timeout) { @@ -48,6 +49,7 @@ function (angular, $, config) { // the rest of the dashboard can load templateValuesSrv.init(dashboard).finally(function() { dynamicDashboardSrv.init(dashboard); + unsavedChangesSrv.init(dashboard, $scope); $scope.dashboard = dashboard; $scope.dashboardMeta = dashboard.meta; diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index 97fa3027cfc..8c1862674e7 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -1,90 +1,68 @@ define([ 'angular', 'lodash', - 'config', ], -function(angular, _, config) { +function(angular, _) { 'use strict'; - if (!config.unsaved_changes_warning) { - return; - } - var module = angular.module('grafana.services'); - module.service('unsavedChangesSrv', function($rootScope, $modal, $q, $location, $timeout, contextSrv) { + module.service('unsavedChangesSrv', function($modal, $q, $location, $timeout, contextSrv, $window) { - var self = this; - var modalScope = $rootScope.$new(); + function Tracker(dashboard, scope) { + var self = this; - $rootScope.$on("dashboard-loaded", function(event, newDashboard) { - // wait for different services to patch the dashboard (missing properties) - $timeout(function() { - self.original = newDashboard.getSaveModelClone(); - self.current = newDashboard; - }, 1200); - }); + this.original = dashboard.getSaveModelClone(); + this.current = dashboard; + this.originalPath = $location.path(); + this.scope = scope; - $rootScope.$on("dashboard-saved", function(event, savedDashboard) { - self.original = savedDashboard.getSaveModelClone(); - self.current = savedDashboard; - self.orignalPath = $location.path(); - }); + // register events + scope.onAppEvent('dashboard-saved', function() { + self.original = self.current.getSaveModelClone(); + self.originalPath = $location.path(); + }); - $rootScope.$on("$routeChangeSuccess", function() { - self.original = null; - self.originalPath = $location.path(); - }); + $window.onbeforeunload = function() { + if (self.ignoreChanges()) { return; } + if (self.hasChanges()) { + return "There are unsaved changes to this dashboard"; + } + }; - this.ignoreChanges = function() { - if (!contextSrv.isEditor) { return true; } - if (!self.current || !self.current.meta) { return true; } - - var meta = self.current.meta; - return !meta.canSave || meta.fromScript || meta.fromFile; - }; - - window.onbeforeunload = function() { - if (self.ignoreChanges()) { return; } - if (self.has_unsaved_changes()) { - return "There are unsaved changes to this dashboard"; - } - }; - - this.init = function() { - $rootScope.$on("$locationChangeStart", function(event, next) { + scope.$on("$locationChangeStart", function(event, next) { // check if we should look for changes if (self.originalPath === $location.path()) { return true; } if (self.ignoreChanges()) { return true; } - if (self.has_unsaved_changes()) { + if (self.hasChanges()) { event.preventDefault(); self.next = next; - $timeout(self.open_modal); + $timeout(function() { + self.open_modal(); + }); } }); + } + + var p = Tracker.prototype; + + // for some dashboards and users + // changes should be ignored + p.ignoreChanges = function() { + if (!this.original) { return false; } + if (!contextSrv.isEditor) { return true; } + if (!this.current || !this.current.meta) { return true; } + + var meta = this.current.meta; + return !meta.canSave || meta.fromScript || meta.fromFile; }; - this.open_modal = function() { - var confirmModal = $modal({ - template: './app/partials/unsaved-changes.html', - modalClass: 'confirm-modal', - persist: true, - show: false, - scope: modalScope, - keyboard: false - }); - - $q.when(confirmModal).then(function(modalEl) { - modalEl.modal('show'); - }); - }; - - this.cleanDashboardFromRepeatedPanelsAndRows = function(dash) { + // remove stuff that should not count in diff + p.cleanDashboardFromIgnoredChanges = function(dash) { dash.rows = _.filter(dash.rows, function(row) { if (row.repeatRowId) { - console.log('filtering out row'); return false; } @@ -101,13 +79,9 @@ function(angular, _, config) { }); }; - this.has_unsaved_changes = function() { - if (!self.original) { - return false; - } - - var current = self.current.getSaveModelClone(); - var original = self.original; + p.hasChanges = function() { + var current = this.current.getSaveModelClone(); + var original = this.original; // ignore timespan changes current.time = original.time = {}; @@ -126,8 +100,8 @@ function(angular, _, config) { } }); - this.cleanDashboardFromRepeatedPanelsAndRows(current); - this.cleanDashboardFromRepeatedPanelsAndRows(original); + this.cleanDashboardFromIgnoredChanges(current); + this.cleanDashboardFromIgnoredChanges(original); // ignore some panel and row stuff current.forEachPanel(function(panel, panelIndex, row, rowIndex) { @@ -165,28 +139,43 @@ function(angular, _, config) { return false; }; - this.goto_next = function() { + p.open_modal = function() { + var tracker = this; + + var modalScope = this.scope.$new(); + modalScope.ignore = function() { + tracker.original = null; + tracker.goto_next(); + }; + + modalScope.save = function() { + tracker.scope.$emit('save-dashboard'); + }; + + var confirmModal = $modal({ + template: './app/partials/unsaved-changes.html', + modalClass: 'confirm-modal', + persist: false, + show: false, + scope: modalScope, + keyboard: false + }); + + $q.when(confirmModal).then(function(modalEl) { + modalEl.modal('show'); + }); + }; + + p.goto_next = function() { var baseLen = $location.absUrl().length - $location.url().length; - var nextUrl = self.next.substring(baseLen); + var nextUrl = this.next.substring(baseLen); $location.url(nextUrl); }; - modalScope.ignore = function() { - self.original = null; - self.goto_next(); + this.Tracker = Tracker; + this.init = function(dashboard, scope) { + // wait for different services to patch the dashboard (missing properties) + $timeout(function() { new Tracker(dashboard, scope); }, 1200); }; - - modalScope.save = function() { - var unregister = $rootScope.$on('dashboard-saved', function() { - self.goto_next(); - }); - - $timeout(unregister, 2000); - - $rootScope.$emit('save-dashboard'); - }; - - }).run(function(unsavedChangesSrv) { - unsavedChangesSrv.init(); }); }); diff --git a/public/test/specs/unsavedChangesSrv-specs.js b/public/test/specs/unsavedChangesSrv-specs.js new file mode 100644 index 00000000000..33691e17a0b --- /dev/null +++ b/public/test/specs/unsavedChangesSrv-specs.js @@ -0,0 +1,48 @@ +define([ + 'features/dashboard/unsavedChangesSrv', + 'features/dashboard/dashboardSrv' +], function() { + 'use strict'; + + describe("unsavedChangesSrv", function() { + var _unsavedChangesSrv; + var _dashboardSrv; + var _location; + var _contextSrvStub = { isEditor: true }; + var _rootScope; + var tracker; + var dash; + var scope; + + beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', _contextSrvStub); + })); + + beforeEach(inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { + _unsavedChangesSrv = unsavedChangesSrv; + _dashboardSrv = dashboardSrv; + _location = $location; + _rootScope = $rootScope; + })); + + beforeEach(function() { + dash = _dashboardSrv.create({}); + scope = _rootScope.$new(); + scope.appEvent = sinon.spy(); + scope.onAppEvent = sinon.spy(); + + tracker = new _unsavedChangesSrv.Tracker(dash, scope); + }); + + it('No changes should not have changes', function() { + expect(tracker.hasChanges()).to.be(false); + }); + + it('Simple change should be registered', function() { + dash.property = "google"; + expect(tracker.hasChanges()).to.be(true); + }); + + }); +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 2dc85cc65ef..9f69735d782 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -141,6 +141,7 @@ require([ 'specs/dashboardViewStateSrv-specs', 'specs/soloPanelCtrl-specs', 'specs/dynamicDashboardSrv-specs', + 'specs/unsavedChangesSrv-specs', ]; var pluginSpecs = (config.plugins.specs || []).map(function (spec) { From 14e8c15a3abffa1d52236d8fc5665d3c0fa73cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 16:42:40 +0200 Subject: [PATCH 23/31] Lots of new unit tests for unsaved changes service --- ' | 47 --------------- .../features/dashboard/unsavedChangesSrv.js | 59 ++++++++----------- public/test/specs/unsavedChangesSrv-specs.js | 38 +++++++++++- 3 files changed, 60 insertions(+), 84 deletions(-) delete mode 100644 ' diff --git a/' b/' deleted file mode 100644 index 2b78b586722..00000000000 --- a/' +++ /dev/null @@ -1,47 +0,0 @@ -define([ - 'features/dashboard/unsavedChangesSrv', - 'features/dashboard/dashboardSrv' -], function() { - 'use strict'; - - describe("unsavedChangesSrv", function() { - var _unsavedChangesSrv; - var _dashboardSrv; - var _location; - var _contextSrvStub = { - isEditor: true - }; - var _rootScope; - var tracker; - - beforeEach(module('grafana.services')); - beforeEach(module(function($provide) { - $provide.value('contextSrv', _contextSrvStub); - })); - - beforeEach(inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { - _unsavedChangesSrv = unsavedChangesSrv; - _dashboardSrv = dashboardSrv; - _location = $location; - _rootScope = $rootScope; - })); - - describe('when dashboard is modified and route changes', function() { - - beforeEach(function() { - var dash = _dashboardSrv.create({}); - var scope = _rootScope.$new(); - scope.appEvent = sinon.spy(); - scope.onAppEvent = sinon.spy(); - tracker = _unsavedChangesSrv.constructor(dash, scope); - }); - - it('No changes should not have changes', function() { - expect(tracker.hasChanges()).to.be(false); - }); - - }); - - }); - -}); diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index 8c1862674e7..fe45fbe48ba 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -61,6 +61,12 @@ function(angular, _) { // remove stuff that should not count in diff p.cleanDashboardFromIgnoredChanges = function(dash) { + // ignore time and refresh + dash.time = 0; + dash.refresh = 0; + dash.version = 0; + + // filter row and panels properties that should be ignored dash.rows = _.filter(dash.rows, function(row) { if (row.repeatRowId) { return false; @@ -70,58 +76,39 @@ function(angular, _) { if (panel.repeatPanelId) { return false; } + // remove scopedVars panel.scopedVars = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + return true; }); + // ignore collapse state + row.collapse = false; return true; }); + + // ignore template variable values + _.each(dash.templating.list, function(value) { + value.current = null; + value.options = null; + }); + }; p.hasChanges = function() { var current = this.current.getSaveModelClone(); var original = this.original; - // ignore timespan changes - current.time = original.time = {}; - current.refresh = original.refresh; - // ignore version - current.version = original.version; - - // ignore template variable values - _.each(current.templating.list, function(value, index) { - value.current = null; - value.options = null; - - if (original.templating.list.length > index) { - original.templating.list[index].current = null; - original.templating.list[index].options = null; - } - }); - this.cleanDashboardFromIgnoredChanges(current); this.cleanDashboardFromIgnoredChanges(original); - // ignore some panel and row stuff - current.forEachPanel(function(panel, panelIndex, row, rowIndex) { - var originalRow = original.rows[rowIndex]; - var originalPanel = original.getPanelById(panel.id); - // ignore row collapse state - if (originalRow) { - row.collapse = originalRow.collapse; - } - if (originalPanel) { - // ignore graph legend sort - if (originalPanel.legend && panel.legend) { - delete originalPanel.legend.sortDesc; - delete originalPanel.legend.sort; - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - } - }); - var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' }); var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' }); diff --git a/public/test/specs/unsavedChangesSrv-specs.js b/public/test/specs/unsavedChangesSrv-specs.js index 33691e17a0b..43cb3ab0e63 100644 --- a/public/test/specs/unsavedChangesSrv-specs.js +++ b/public/test/specs/unsavedChangesSrv-specs.js @@ -17,6 +17,7 @@ define([ beforeEach(module('grafana.services')); beforeEach(module(function($provide) { $provide.value('contextSrv', _contextSrvStub); + $provide.value('$window', {}); })); beforeEach(inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { @@ -27,7 +28,13 @@ define([ })); beforeEach(function() { - dash = _dashboardSrv.create({}); + dash = _dashboardSrv.create({ + rows: [ + { + panels: [{ test: "asd", legend: { } }] + } + ] + }); scope = _rootScope.$new(); scope.appEvent = sinon.spy(); scope.onAppEvent = sinon.spy(); @@ -44,5 +51,34 @@ define([ expect(tracker.hasChanges()).to.be(true); }); + it('Should ignore a lot of changes', function() { + dash.time = {from: '1h'}; + dash.refresh = true; + dash.version = 10; + dash.rows[0].collapse = true; + expect(tracker.hasChanges()).to.be(false); + }); + + it('Should ignore row collapse change', function() { + dash.rows[0].collapse = true; + expect(tracker.hasChanges()).to.be(false); + }); + + it('Should ignore panel legend changes', function() { + dash.rows[0].panels[0].legend.sortDesc = true; + dash.rows[0].panels[0].legend.sort = "avg"; + expect(tracker.hasChanges()).to.be(false); + }); + + it('Should ignore panel repeats', function() { + dash.rows[0].panels.push({repeatPanelId: 10}); + expect(tracker.hasChanges()).to.be(false); + }); + + it('Should ignore row repeats', function() { + dash.rows.push({repeatRowId: 10}); + expect(tracker.hasChanges()).to.be(false); + }); + }); }); From 53cb0feda9ffd176c3f5700a2958606142162ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 17:28:34 +0200 Subject: [PATCH 24/31] Various fixes, restored search auto focus --- public/app/directives/giveFocus.js | 26 +++++++++++++++++++ .../features/dashboard/unsavedChangesSrv.js | 2 +- public/test/specs/unsavedChangesSrv-specs.js | 3 +-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/public/app/directives/giveFocus.js b/public/app/directives/giveFocus.js index e69de29bb2d..ef395d27fbd 100644 --- a/public/app/directives/giveFocus.js +++ b/public/app/directives/giveFocus.js @@ -0,0 +1,26 @@ +define([ + 'angular' +], +function (angular) { + 'use strict'; + + angular.module('grafana.directives').directive('giveFocus', function() { + return function(scope, element, attrs) { + element.click(function(e) { + e.stopPropagation(); + }); + + scope.$watch(attrs.giveFocus,function (newValue) { + if (!newValue) { + return; + } + setTimeout(function() { + element.focus(); + var pos = element.val().length * 2; + element[0].setSelectionRange(pos, pos); + }, 200); + },true); + }; + }); + +}); diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index fe45fbe48ba..11a806c865d 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -64,7 +64,7 @@ function(angular, _) { // ignore time and refresh dash.time = 0; dash.refresh = 0; - dash.version = 0; + dash.schemaVersion = 0; // filter row and panels properties that should be ignored dash.rows = _.filter(dash.rows, function(row) { diff --git a/public/test/specs/unsavedChangesSrv-specs.js b/public/test/specs/unsavedChangesSrv-specs.js index 43cb3ab0e63..4ec2a8f82a1 100644 --- a/public/test/specs/unsavedChangesSrv-specs.js +++ b/public/test/specs/unsavedChangesSrv-specs.js @@ -54,8 +54,7 @@ define([ it('Should ignore a lot of changes', function() { dash.time = {from: '1h'}; dash.refresh = true; - dash.version = 10; - dash.rows[0].collapse = true; + dash.schemaVersion = 10; expect(tracker.hasChanges()).to.be(false); }); From 8c14e565a7bdee9619dbf396d7b9a560fa37b2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 28 Apr 2015 17:54:22 +0200 Subject: [PATCH 25/31] Restored the variable color for the label/name in the submenu --- public/app/directives/templateParamSelector.js | 8 ++------ .../features/dashboard/partials/variableValueSelect.html | 3 +++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/app/directives/templateParamSelector.js b/public/app/directives/templateParamSelector.js index 3d7a9f964d4..629c3ececcc 100644 --- a/public/app/directives/templateParamSelector.js +++ b/public/app/directives/templateParamSelector.js @@ -99,12 +99,8 @@ function (angular, app, _) { }; scope.updateLinkText = function() { - scope.linkText = ""; - if (!variable.hideLabel) { - scope.linkText = (variable.label || variable.name) + ': '; - } - - scope.linkText += variable.current.text; + scope.labelText = variable.label || '$' + variable.name; + scope.linkText = variable.current.text; }; scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 1904aa598ee..058500c495c 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,3 +1,6 @@ + + {{labelText}}: + {{linkText}} From c5be95e46ca36652cf5c7f63accedbc8401e6c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 14:23:29 +0200 Subject: [PATCH 26/31] Began polish and tweaks of new template variable multi select dropdown --- .../partials/variableValueSelect.html | 5 +- public/css/less/submenu.less | 58 +++++++++++-------- public/css/less/variables.dark.less | 2 +- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 058500c495c..ffa5b46ada9 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -7,10 +7,9 @@
    -
    +
    - +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index 693c29b3ea4..cdf231cbd9f 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -28,37 +28,49 @@ position: absolute; top: 43px; min-width: 200px; - height: 400px; + max-height: 400px; background: @grafanaPanelBackground; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; - padding: 10px; + font-size: @baseFontSize; + padding: 4px 4px 8px 4px; +} - .variable-options-container { - height: 350px; - overflow: auto; - display: block; - line-height: 28px; +.variable-options-container { + max-height: 350px; + overflow: auto; + display: block; + line-height: 28px; +} + +.variable-option { + display: block; + .fa { + font-size: 130%; + position: relative; + top: 2px; + padding-right: 4px; } + .fa-check-square-o { display: none; } - .variable-option { - display: block; - .fa { - font-size: 130%; - position: relative; - top: 2px; - padding-right: 6px; + &.selected { + .fa-square-o { + display: none; } - .fa-check-square-o { display: none; } - - &.selected { - .fa-square-o { - display: none; - } - .fa-check-square-o { - display: inline-block; - } + .fa-check-square-o { + display: inline-block; } } } + +.variable-search-wrapper { + input { + width: 100%; + padding: 7px 8px; + height: 100%; + box-sizing: border-box; + margin-bottom: 6px; + } +} + diff --git a/public/css/less/variables.dark.less b/public/css/less/variables.dark.less index 9a0180added..40333bef9ab 100644 --- a/public/css/less/variables.dark.less +++ b/public/css/less/variables.dark.less @@ -63,7 +63,7 @@ @monoFontFamily: Menlo, Monaco, Consolas, "Courier New", monospace; @baseFontSize: 14px; -@baseFontWeight: 400; +@baseFontWeight: 400; @baseFontFamily: @sansFontFamily; @baseLineHeight: 20px; @altFontFamily: @serifFontFamily; From fe0bf876d92bb618f648d098cb7e4bc99c50bb44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 15:34:14 +0200 Subject: [PATCH 27/31] Style changes and polish to multi variable value selection, #1144 --- .../app/directives/templateParamSelector.js | 34 ++++++++++++++----- .../partials/variableValueSelect.html | 6 ++-- public/css/less/submenu.less | 32 +++++++++-------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/public/app/directives/templateParamSelector.js b/public/app/directives/templateParamSelector.js index 629c3ececcc..1f1cf9faf5f 100644 --- a/public/app/directives/templateParamSelector.js +++ b/public/app/directives/templateParamSelector.js @@ -42,26 +42,38 @@ function (angular, app, _) { }, 0, false); }; - scope.optionSelected = function(option) { + scope.optionSelected = function(option, event) { option.selected = !option.selected; - if (!variable.multi || option.text === 'All') { + var hideAfter = true; + var setAllExceptCurrentTo = function(newValue) { _.each(scope.options, function(other) { - if (option !== other) { - other.selected = false; - } + if (option !== other) { other.selected = newValue; } }); + }; + + if (option.text === 'All') { + setAllExceptCurrentTo(false); + } + else if (!variable.multi) { + setAllExceptCurrentTo(false); + } else { + if (event.ctrlKey || event.metaKey || event.shiftKey) { + hideAfter = false; + } + else { + setAllExceptCurrentTo(false); + } } var selected = _.filter(scope.options, {selected: true}); - // enfore the first selected if no option is selected if (selected.length === 0) { - scope.options[0].selected = true; - selected = [scope.options[0]]; + option.selected = true; + selected = [option]; } - if (selected.length > 1) { + if (selected.length > 1 && selected.length !== scope.options.length) { if (selected[0].text === 'All') { selected[0].selected = false; selected = selected.slice(1, selected.length); @@ -80,6 +92,10 @@ function (angular, app, _) { scope.updateLinkText(); scope.onUpdated(); + + if (hideAfter) { + scope.hide(); + } }; scope.hide = function() { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index ffa5b46ada9..a556b57a72d 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -14,11 +14,9 @@
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index cdf231cbd9f..35cf84022cb 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -26,40 +26,42 @@ .variable-value-dropdown { position: absolute; - top: 43px; - min-width: 200px; + top: 35px; + min-width: 150px; max-height: 400px; background: @grafanaPanelBackground; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; font-size: @baseFontSize; - padding: 4px 4px 8px 4px; + padding: 0; + border-radius: 3px 3px 0 0; } .variable-options-container { max-height: 350px; overflow: auto; display: block; - line-height: 28px; + line-height: 26px; } .variable-option { display: block; - .fa { - font-size: 130%; - position: relative; - top: 2px; - padding-right: 4px; + padding: 0 8px; + + &:hover { + background-color: @blueDark; + } + + .fa { + line-height: 26px; + float: right; + padding-left: 4px; } - .fa-check-square-o { display: none; } &.selected { - .fa-square-o { - display: none; - } - .fa-check-square-o { - display: inline-block; + .variable-option-icon:before { + content: "\f00c"; } } } From 3ee1ea28e152b0a435289f9ab2e538a80ba4f5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 30 Apr 2015 09:09:00 +0200 Subject: [PATCH 28/31] Templating: Support for search filtering and keyboard up/down filtering in the new multi variable value selector dropdown, #1144 --- .../app/directives/templateParamSelector.js | 38 +++++++++++++++++-- .../partials/variableValueSelect.html | 5 ++- public/css/less/submenu.less | 2 +- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/public/app/directives/templateParamSelector.js b/public/app/directives/templateParamSelector.js index 1f1cf9faf5f..fa0e56acebc 100644 --- a/public/app/directives/templateParamSelector.js +++ b/public/app/directives/templateParamSelector.js @@ -21,9 +21,15 @@ function (angular, app, _) { var variable = scope.variable; scope.show = function() { + if (scope.selectorOpen) { + return; + } + scope.selectorOpen = true; scope.giveFocus = 1; scope.oldCurrentText = variable.current.text; + scope.highlightIndex = -1; + var currentValues = variable.current.value; if (_.isString(currentValues)) { @@ -37,11 +43,39 @@ function (angular, app, _) { return option; }); + scope.search = {query: '', options: scope.options}; + $timeout(function() { bodyEl.on('click', scope.bodyOnClick); }, 0, false); }; + scope.queryChanged = function() { + scope.highlightIndex = -1; + scope.search.options = _.filter(scope.options, function(option) { + return option.text.toLowerCase().indexOf(scope.search.query.toLowerCase()) !== -1; + }); + }; + + scope.keyDown = function (evt) { + if (evt.keyCode === 27) { + scope.hide(); + } + if (evt.keyCode === 40) { + scope.moveHighlight(1); + } + if (evt.keyCode === 38) { + scope.moveHighlight(-1); + } + if (evt.keyCode === 13) { + scope.optionSelected(scope.search.options[scope.highlightIndex], {}); + } + }; + + scope.moveHighlight = function(direction) { + scope.highlightIndex = (scope.highlightIndex + direction) % scope.search.options.length; + }; + scope.optionSelected = function(option, event) { option.selected = !option.selected; @@ -100,10 +134,6 @@ function (angular, app, _) { scope.hide = function() { scope.selectorOpen = false; - // if (scope.oldCurrentText !== variable.current.text) { - // scope.onUpdated(); - // } - bodyEl.off('click', scope.bodyOnClick); }; diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index a556b57a72d..ff5291c11d3 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -9,12 +9,13 @@
    - +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index 35cf84022cb..f12de218d8b 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -49,7 +49,7 @@ display: block; padding: 0 8px; - &:hover { + &:hover, &.highlighted { background-color: @blueDark; } From 25ef49494b5e6f283deccba6df0c39d569f9576d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 30 Apr 2015 10:47:51 +0200 Subject: [PATCH 29/31] Final polish on repeat panel variable selection, #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 50 +++++++-------- public/app/panels/graph/axisEditor.html | 2 +- public/app/partials/panelgeneral.html | 61 +++++++++++++------ .../test/specs/dynamicDashboardSrv-specs.js | 8 +-- 4 files changed, 74 insertions(+), 47 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 2ecd526f795..51309a0841d 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -11,29 +11,31 @@ function (angular, _) { var self = this; this.init = function(dashboard) { - this.dashboard = dashboard; this.iteration = new Date().getTime(); - - this.handlePanelRepeats(dashboard); - this.handleRowRepeats(dashboard); + this.process(dashboard); }; this.update = function(dashboard) { - this.dashboard = dashboard; this.iteration = this.iteration + 1; - - this.handlePanelRepeats(dashboard); - this.handleRowRepeats(dashboard); + this.process(dashboard); }; - this.handlePanelRepeats = function(dashboard) { + this.process = function(dashboard) { + if (dashboard.templating.list.length === 0) { return; } + this.dashboard = dashboard; + + this.handlePanelRepeats(); + this.handleRowRepeats(); + }; + + this.handlePanelRepeats = function() { var i, j, row, panel; - for (i = 0; i < dashboard.rows.length; i++) { - row = dashboard.rows[i]; + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.repeat) { - this.repeatPanel(panel, row, dashboard); + this.repeatPanel(panel, row); } // clean up old left overs else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { @@ -44,16 +46,16 @@ function (angular, _) { } }; - this.handleRowRepeats = function(dashboard) { + this.handleRowRepeats = function() { var i, row; - for (i = 0; i < dashboard.rows.length; i++) { - row = dashboard.rows[i]; + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; if (row.repeat) { - this.repeatRow(row, dashboard); + this.repeatRow(row); } // clean up old left overs else if (row.repeatRowId && row.repeatIteration !== this.iteration) { - dashboard.rows.splice(i, 1); + this.dashboard.rows.splice(i, 1); i = i - 1; } } @@ -93,8 +95,8 @@ function (angular, _) { return copy; }; - this.repeatRow = function(row, dashboard) { - var variables = dashboard.templating.list; + this.repeatRow = function(row) { + var variables = this.dashboard.templating.list; var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); if (!variable) { return; @@ -152,12 +154,10 @@ function (angular, _) { return clone; }; - this.repeatPanel = function(panel, row, dashboard) { - var variables = dashboard.templating.list; - var variable = _.findWhere(variables, {name: panel.repeat.replace('$', '')}); - if (!variable) { - return; - } + this.repeatPanel = function(panel, row) { + var variables = this.dashboard.templating.list; + var variable = _.findWhere(variables, {name: panel.repeat}); + if (!variable) { return; } var selected; if (variable.current.text === 'All') { diff --git a/public/app/panels/graph/axisEditor.html b/public/app/panels/graph/axisEditor.html index ae69ec60929..d161330f5c0 100644 --- a/public/app/panels/graph/axisEditor.html +++ b/public/app/panels/graph/axisEditor.html @@ -190,7 +190,7 @@
      -
    • +
    • Legend values
    • diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index 2fd3f0043cb..8d4ef57b141 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -1,24 +1,51 @@
      General options
      -
      - -
      -
      - -
      -
      - -
      - -
      +
      +
        +
      • + Title +
      • +
      • + +
      • +
      • + Span +
      • +
      • + +
      • +
      • + Height +
      • +
      • + +
      • +
      • + + + +
      • +
      +
      +
      +
      -
      Templating options
      -
      - - -
      -
      +
      Templating options
      +
      +
        +
      • + Repeat Panel +
      • +
      • + +
      • +
      +
      +
      +
    diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 968a181e288..08a2f1af561 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -36,7 +36,7 @@ define([ dynamicDashScenario('given dashboard with panel repeat', function(ctx) { ctx.setup(function(dash) { dash.rows.push({ - panels: [{id: 2, repeat: '$apps'}] + panels: [{id: 2, repeat: 'apps'}] }); dash.templating.list.push({ name: 'apps', @@ -56,7 +56,7 @@ define([ }); it('should mark panel repeated', function() { - expect(ctx.rows[0].panels[0].repeat).to.be('$apps'); + expect(ctx.rows[0].panels[0].repeat).to.be('apps'); expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); }); @@ -103,7 +103,7 @@ define([ dynamicDashScenario('given dashboard with row repeat', function(ctx) { ctx.setup(function(dash) { dash.rows.push({ - repeat: '$servers', + repeat: 'servers', panels: [{id: 2}] }); dash.templating.list.push({ @@ -128,7 +128,7 @@ define([ }); it('should mark second row as repeated', function() { - expect(ctx.rows[0].repeat).to.be('$servers'); + expect(ctx.rows[0].repeat).to.be('servers'); }); it('should clear repeat field on repeated row', function() { From 32e0ce1beb8c738fae6e08784be441e8e0c3e240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 30 Apr 2015 10:50:23 +0200 Subject: [PATCH 30/31] Minor code refinements to panel repeat code, #1888 --- .../features/dashboard/dynamicDashboardSrv.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 51309a0841d..cfa824e3402 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -24,14 +24,11 @@ function (angular, _) { if (dashboard.templating.list.length === 0) { return; } this.dashboard = dashboard; - this.handlePanelRepeats(); - this.handleRowRepeats(); - }; - - this.handlePanelRepeats = function() { var i, j, row, panel; for (i = 0; i < this.dashboard.rows.length; i++) { row = this.dashboard.rows[i]; + + // repeat panels first for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.repeat) { @@ -43,13 +40,8 @@ function (angular, _) { j = j - 1; } } - } - }; - this.handleRowRepeats = function() { - var i, row; - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; + // handle row repeats if (row.repeat) { this.repeatRow(row); } @@ -61,6 +53,7 @@ function (angular, _) { } }; + // returns a new row clone or reuses a clone from previous iteration this.getRowClone = function(sourceRow, index) { if (index === 0) { return sourceRow; @@ -95,6 +88,7 @@ function (angular, _) { return copy; }; + // returns a new panel clone or reuses a clone from previous iteration this.repeatRow = function(row) { var variables = this.dashboard.templating.list; var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); From e17f56b4ad480eeb6b12d8641f446735bc040e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 30 Apr 2015 11:15:26 +0200 Subject: [PATCH 31/31] Final polish on panel & row repeats, #1888, still some missing places where scopedVars needs to be used --- .../partials/variableValueSelect.html | 30 ++++++------ public/app/partials/panelgeneral.html | 2 +- public/app/partials/roweditor.html | 47 ++++++++++++++----- public/css/less/submenu.less | 2 +- 4 files changed, 53 insertions(+), 28 deletions(-) diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index ff5291c11d3..30a61072626 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,24 +1,26 @@ {{labelText}}: -
    - {{linkText}} - - - diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index 8d4ef57b141..7a4b57b0db8 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -38,7 +38,7 @@ Repeat Panel
  • -
  • diff --git a/public/app/partials/roweditor.html b/public/app/partials/roweditor.html index f47351c8ffa..37e258a9b89 100644 --- a/public/app/partials/roweditor.html +++ b/public/app/partials/roweditor.html @@ -1,3 +1,4 @@ +
    @@ -16,24 +17,46 @@
    -
    +
    Row details
    -
    - +
    +
      +
    • + Title +
    • +
    • + +
    • +
    • + Height +
    • +
    • + +
    • +
    • + + + +
    • +
    +
    -
    - -
    - -
    -
    Templating options
    -
    - - +
    +
      +
    • + Repeat Row +
    • +
    • + +
    • +
    +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index f12de218d8b..f69ac9c5791 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -26,7 +26,7 @@ .variable-value-dropdown { position: absolute; - top: 35px; + top: 27px; min-width: 150px; max-height: 400px; background: @grafanaPanelBackground;