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 001/398] 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 002/398] 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 003/398] 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 004/398] 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 005/398] 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 006/398] 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 009/398] 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 010/398] 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 011/398] 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 618d4f0a9de54bddd603e12e3a8f74f6004214b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Mar 2015 13:49:05 +0100 Subject: [PATCH 012/398] Testing kariosdb datasource, hm.. needs a lot of work --- pkg/api/frontendsettings.go | 6 +- .../plugins/datasource/kairosdb/datasource.js | 420 ++++++++++++++++++ .../datasource/kairosdb/partials/config.html | 1 + .../kairosdb/partials/query.editor.html | 384 ++++++++++++++++ .../plugins/datasource/kairosdb/plugin.json | 17 + .../plugins/datasource/kairosdb/queryCtrl.js | 379 ++++++++++++++++ 6 files changed, 1204 insertions(+), 3 deletions(-) create mode 100644 src/app/plugins/datasource/kairosdb/datasource.js create mode 100644 src/app/plugins/datasource/kairosdb/partials/config.html create mode 100644 src/app/plugins/datasource/kairosdb/partials/query.editor.html create mode 100644 src/app/plugins/datasource/kairosdb/plugin.json create mode 100644 src/app/plugins/datasource/kairosdb/queryCtrl.js diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index e42a2deedf2..3af191a7af7 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -1,11 +1,10 @@ package api import ( - "errors" - "fmt" "strconv" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -45,7 +44,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro meta, exists := plugins.DataSources[ds.Type] if !exists { - return nil, errors.New(fmt.Sprintf("Could not find plugin definition for data source: %v", ds.Type)) + log.Error(3, "Could not find plugin definition for data source: %v", ds.Type) + continue } dsMap["meta"] = meta diff --git a/src/app/plugins/datasource/kairosdb/datasource.js b/src/app/plugins/datasource/kairosdb/datasource.js new file mode 100644 index 00000000000..e055c118956 --- /dev/null +++ b/src/app/plugins/datasource/kairosdb/datasource.js @@ -0,0 +1,420 @@ +define([ + 'angular', + 'lodash', + 'kbn', + './queryCtrl', +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.services'); + var tagList = null; + + module.factory('KairosDBDatasource', function($q, $http) { + + function KairosDBDatasource(datasource) { + this.type = datasource.type; + this.editorSrc = 'plugins/datasources/kairosdb/kairosdb.editor.html'; + this.url = datasource.url; + this.name = datasource.name; + this.supportMetrics = true; + this.grafanaDB = datasource.grafanaDB; + } + + // Called once per panel (graph) + KairosDBDatasource.prototype.query = function(options) { + var start = options.range.from; + var end = options.range.to; + + var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options))); + var plotParams = _.compact(_.map(options.targets, function(target){ + var alias = target.alias; + if (typeof target.alias == 'undefined' || target.alias == "") + alias = target.metric; + return !target.hide + ? {alias: alias, + exouter: target.exOuter} + : null; + })); + var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams); + // No valid targets, return the empty result to save a round trip. + if (_.isEmpty(queries)) { + var d = $q.defer(); + d.resolve({ data: [] }); + return d.promise; + } + return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias,handleQueryError); + }; + + /////////////////////////////////////////////////////////////////////// + /// Query methods + /////////////////////////////////////////////////////////////////////// + + KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { + var reqBody = { + metrics: queries + }; + reqBody.cache_time=0; + convertToKairosTime(start,reqBody,'start'); + convertToKairosTime(end,reqBody,'end'); + var options = { + method: 'POST', + url: '/api/v1/datapoints/query', + data: reqBody + }; + + options.url = this.url + options.url; + return $http(options); + }; + + /** + * Gets the list of metrics + * @returns {*|Promise} + */ + KairosDBDatasource.prototype.performMetricSuggestQuery = function() { + var options = { + url : this.url + '/api/v1/metricnames', + method : 'GET' + }; + return $http(options).then(function(results) { + if (!results.data) { + return []; + } + return results.data.results; + }); + + }; + + KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname,range,type,keyValue) { + if(tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && + (range.to === tagList.range.to)) { + return getTagListFromResponse(tagList.results,type,keyValue); + } + tagList = { + metricName:metricname, + range:range + }; + var body = { + metrics : [{name : metricname}] + }; + convertToKairosTime(range.from,body,'start'); + convertToKairosTime(range.to,body,'end'); + var options = { + url : this.url + '/api/v1/datapoints/query/tags', + method : 'POST', + data : body + }; + return $http(options).then(function(results) { + tagList.results = results; + return getTagListFromResponse(results,type,keyValue); + }); + + }; + + ///////////////////////////////////////////////////////////////////////// + /// Formatting methods + //////////////////////////////////////////////////////////////////////// + + function getTagListFromResponse(results,type,keyValue) { + if (!results.data) { + return []; + } + if(type==="key") { + return _.keys(results.data.queries[0].results[0].tags); + } + else if(type==="value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { + return results.data.queries[0].results[0].tags[keyValue]; + } + return []; + } + + /** + * Requires a verion of KairosDB with every CORS defects fixed + * @param results + * @returns {*} + */ + function handleQueryError(results) { + if(results.data.errors && !_.isEmpty(results.data.errors)) { + var errors = { + message: results.data.errors[0] + }; + return $q.reject(errors); + } + else{ + return $q.reject(results); + } + } + + function handleKairosDBQueryResponse(plotParams, results) { + var output = []; + var index = 0; + _.each(results.data.queries, function (series) { + var sample_size = series.sample_size; + console.log("sample_size:" + sample_size + " samples"); + + _.each(series.results, function (result) { + + //var target = result.name; + var target = plotParams[index].alias; + var details = " ( "; + _.each(result.group_by,function(element) { + if(element.name==="tag") { + _.each(element.group,function(value, key) { + details+= key+"="+value+" "; + }); + } + else if(element.name==="value") { + details+= 'value_group='+element.group.group_number+" "; + } + else if(element.name==="time") { + details+= 'time_group='+element.group.group_number+" "; + } + }); + details+= ") "; + if (details != " ( ) ") + target += details; + var datapoints = []; + + for (var i = 0; i < result.values.length; i++) { + var t = Math.floor(result.values[i][0]); + var v = result.values[i][1]; + datapoints[i] = [v, t]; + } + if (plotParams[index].exouter) + datapoints = PeakFilter(datapoints, 10); + output.push({ target: target, datapoints: datapoints }); + }); + index ++; + }); + var output2 = { data: _.flatten(output) }; + + return output2; + } + + function convertTargetToQuery(options,target) { + if (!target.metric || target.hide) { + return null; + } + + var query = { + name: target.metric + }; + + query.aggregators = []; + if(target.downsampling!=='(NONE)') { + query.aggregators.push({ + name: target.downsampling, + align_sampling: true, + align_start_time: true, + sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval) + }); + } + if(target.horizontalAggregators) { + _.each(target.horizontalAggregators,function(chosenAggregator) { + var returnedAggregator = { + name:chosenAggregator.name + }; + if(chosenAggregator.sampling_rate) { + returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); + returnedAggregator.align_sampling = true; + returnedAggregator.align_start_time=true; + } + if(chosenAggregator.unit) { + returnedAggregator.unit = chosenAggregator.unit+'s'; + } + if(chosenAggregator.factor && chosenAggregator.name==='div') { + returnedAggregator.divisor = chosenAggregator.factor; + } + else if(chosenAggregator.factor && chosenAggregator.name==='scale') { + returnedAggregator.factor = chosenAggregator.factor; + } + if(chosenAggregator.percentile) { + returnedAggregator.percentile = chosenAggregator.percentile; + } + query.aggregators.push(returnedAggregator); + }); + } + if(_.isEmpty(query.aggregators)) { + delete query.aggregators; + } + + if(target.tags) { + query.tags = angular.copy(target.tags); + } + + if(target.groupByTags || target.nonTagGroupBys) { + query.group_by = []; + if(target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});} + if(target.nonTagGroupBys) { + _.each(target.nonTagGroupBys,function(rawGroupBy) { + var formattedGroupBy = angular.copy(rawGroupBy); + if(formattedGroupBy.name==='time') { + formattedGroupBy.range_size=KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); + } + query.group_by.push(formattedGroupBy); + }); + } + } + return query; + } + + /////////////////////////////////////////////////////////////////////// + /// Time conversion functions specifics to KairosDB + ////////////////////////////////////////////////////////////////////// + + KairosDBDatasource.prototype.convertToKairosInterval = function(intervalString) { + var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/; + var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/; + var matches = intervalString.match(interval_regex_ms); + if(!matches) { + matches = intervalString.match(interval_regex); + } + if (!matches) { + throw new Error('Invalid interval string, expecting a number followed by one of "y M w d h m s ms"'); + } + + var value = matches[1]; + var unit = matches[2]; + if (value%1!==0) { + if(unit==='ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} + value = Math.round(kbn.intervals_in_seconds[unit]*value*1000); + unit = 'ms'; + + } + switch(unit) { + case 'ms': + unit = 'milliseconds'; + break; + case 's': + unit = 'seconds'; + break; + case 'm': + unit = 'minutes'; + break; + case 'h': + unit = 'hours'; + break; + case 'd': + unit = 'days'; + break; + case 'w': + unit = 'weeks'; + break; + case 'M': + unit = 'months'; + break; + case 'y': + unit = 'years'; + break; + default: + console.log("Unknown interval ", intervalString); + break; + } + + return { + "value": value, + "unit": unit + }; + + }; + + function convertToKairosTime(date, response_obj, start_stop_name) { + var name; + if (_.isString(date)) { + if (date === 'now') { + return; + } + else if (date.indexOf('now-') >= 0) { + + name = start_stop_name + "_relative"; + + date = date.substring(4); + var re_date = /(\d+)\s*(\D+)/; + var result = re_date.exec(date); + if (result) { + var value = result[1]; + var unit = result[2]; + switch(unit) { + case 'ms': + unit = 'milliseconds'; + break; + case 's': + unit = 'seconds'; + break; + case 'm': + unit = 'minutes'; + break; + case 'h': + unit = 'hours'; + break; + case 'd': + unit = 'days'; + break; + case 'w': + unit = 'weeks'; + break; + case 'M': + unit = 'months'; + break; + case 'y': + unit = 'years'; + break; + default: + console.log("Unknown date ", date); + break; + } + response_obj[name] = { + "value": value, + "unit": unit + }; + return; + } + console.log("Unparseable date", date); + return; + } + date = kbn.parseDate(date); + } + + if(_.isDate(date)) { + name = start_stop_name + "_absolute"; + response_obj[name] = date.getTime(); + return; + } + + console.log("Date is neither string nor date"); + } + + function PeakFilter(dataIn, limit) { + var datapoints = dataIn; + var arrLength = datapoints.length; + if (arrLength <= 3) + return datapoints; + var LastIndx = arrLength - 1; + + // Check first point + var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]); + var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]); + if (prvDelta >= limit && nxtDelta < limit) + datapoints[0][0] = datapoints[1][0]; + + // Check last point + prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]); + nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]); + if (prvDelta >= limit && nxtDelta < limit) + datapoints[LastIndx][0] = datapoints[LastIndx - 1][0]; + + for (var i = 1; i < arrLength - 1; i++){ + prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); + nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); + if (prvDelta >= limit && nxtDelta >= limit) + datapoints[i][0] = (datapoints[i-1][0] + datapoints[i+1][0]) / 2; + } + + return datapoints; + } + + //////////////////////////////////////////////////////////////////////// + return KairosDBDatasource; + }); + +}); diff --git a/src/app/plugins/datasource/kairosdb/partials/config.html b/src/app/plugins/datasource/kairosdb/partials/config.html new file mode 100644 index 00000000000..384edeaeafe --- /dev/null +++ b/src/app/plugins/datasource/kairosdb/partials/config.html @@ -0,0 +1 @@ +
    diff --git a/src/app/plugins/datasource/kairosdb/partials/query.editor.html b/src/app/plugins/datasource/kairosdb/partials/query.editor.html new file mode 100644 index 00000000000..8a794e7fdf2 --- /dev/null +++ b/src/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -0,0 +1,384 @@ +
    +
    + +
    + + +
      +
    • + {{targetLetters[$index]}} +
    • +
    • + + + +
    • +
    • + +
    • +
    • + + + + +
    • + +
    • +  Peak filter + +
    • +
    + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
      +
    • + +
    • + +
    • + Downsampling with +
    • +
    • + +
    • + + +
    • + every +
    • +
    • + + + + +
    • +
    +
    +
    +
    diff --git a/src/app/plugins/datasource/kairosdb/plugin.json b/src/app/plugins/datasource/kairosdb/plugin.json new file mode 100644 index 00000000000..bdbf27c5fa8 --- /dev/null +++ b/src/app/plugins/datasource/kairosdb/plugin.json @@ -0,0 +1,17 @@ +{ + "pluginType": "datasource", + "name": "KairosDB", + + "type": "kairosdb", + "serviceName": "KairosDBDatasource", + + "module": "plugins/datasource/kairosdb/datasource", + + "partials": { + "config": "app/plugins/datasource/kairosdb/partials/config.html", + "query": "app/plugins/datasource/kairosdb/partials/query.editor.html" + }, + + "metrics": true, + "annotations": false +} diff --git a/src/app/plugins/datasource/kairosdb/queryCtrl.js b/src/app/plugins/datasource/kairosdb/queryCtrl.js new file mode 100644 index 00000000000..fef1e4d39f4 --- /dev/null +++ b/src/app/plugins/datasource/kairosdb/queryCtrl.js @@ -0,0 +1,379 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + var metricList = null; + var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; + + module.controller('KairosDBTargetCtrl', function($scope) { + + $scope.init = function() { + $scope.metric = { + list: ["Loading..."], + value: "Loading..." + }; + $scope.panel.stack = false; + if (!$scope.panel.downsampling) { + $scope.panel.downsampling = 'avg'; + } + if (!$scope.target.downsampling) { + $scope.target.downsampling = $scope.panel.downsampling; + $scope.target.sampling = $scope.panel.sampling; + } + $scope.targetLetters = targetLetters; + $scope.updateMetricList(); + $scope.target.errors = validateTarget($scope.target); + }; + + $scope.targetBlur = function() { + $scope.target.metric = $scope.metric.value; + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + $scope.panelBlur = function() { + _.each($scope.panel.targets, function(target) { + target.downsampling = $scope.panel.downsampling; + target.sampling = $scope.panel.sampling; + }); + $scope.get_data(); + }; + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + ////////////////////////////// + // SUGGESTION QUERIES + ////////////////////////////// + + $scope.updateMetricList = function() { + $scope.metricListLoading = true; + metricList = []; + $scope.datasource.performMetricSuggestQuery().then(function(series) { + metricList = series; + $scope.metric.list = series; + if ($scope.target.metric) + $scope.metric.value = $scope.target.metric; + else + $scope.metric.value = ""; + $scope.metricListLoading = false; + return metricList; + }); + }; + + $scope.suggestTagKeys = function(query, callback) { + $scope.updateTimeRange(); + callback($scope.datasource + .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'key','')); + + }; + + $scope.suggestTagValues = function(query, callback) { + callback($scope.datasource + .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'value',$scope.target.currentTagKey)); + }; + + ////////////////////////////// + // FILTER by TAG + ////////////////////////////// + + $scope.addFilterTag = function() { + if (!$scope.addFilterTagMode) { + $scope.addFilterTagMode = true; + $scope.validateFilterTag(); + return; + } + + if (!$scope.target.tags) { + $scope.target.tags = {}; + } + + $scope.validateFilterTag(); + if (!$scope.target.errors.tags) { + if(!_.has($scope.target.tags,$scope.target.currentTagKey)) { + $scope.target.tags[$scope.target.currentTagKey] = []; + } + $scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue); + $scope.target.currentTagKey = ''; + $scope.target.currentTagValue = ''; + $scope.targetBlur(); + } + + $scope.addFilterTagMode = false; + }; + + $scope.removeFilterTag = function(key) { + delete $scope.target.tags[key]; + if(_.size($scope.target.tags)===0) { + $scope.target.tags = null; + } + $scope.targetBlur(); + }; + + $scope.validateFilterTag = function() { + $scope.target.errors.tags = null; + if(!$scope.target.currentTagKey || !$scope.target.currentTagValue) { + $scope.target.errors.tags = "You must specify a tag name and value."; + } + }; + + ////////////////////////////// + // GROUP BY + ////////////////////////////// + + $scope.addGroupBy = function() { + if (!$scope.addGroupByMode) { + $scope.addGroupByMode = true; + $scope.target.currentGroupByType = 'tag'; + $scope.isTagGroupBy = true; + $scope.validateGroupBy(); + return; + } + $scope.validateGroupBy(); + // nb: if error is found, means that user clicked on cross : cancels input + if (_.isEmpty($scope.target.errors.groupBy)) { + if($scope.isTagGroupBy) { + if (!$scope.target.groupByTags) { + $scope.target.groupByTags = []; + } + console.log($scope.target.groupBy.tagKey); + if (!_.contains($scope.target.groupByTags, $scope.target.groupBy.tagKey)) { + $scope.target.groupByTags.push($scope.target.groupBy.tagKey); + $scope.targetBlur(); + } + $scope.target.groupBy.tagKey = ''; + } + else { + if (!$scope.target.nonTagGroupBys) { + $scope.target.nonTagGroupBys = []; + } + var groupBy = { + name: $scope.target.currentGroupByType + }; + if($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;} + else if($scope.isTimeGroupBy) { + groupBy.range_size = $scope.target.groupBy.timeInterval; + groupBy.group_count = $scope.target.groupBy.groupCount; + } + $scope.target.nonTagGroupBys.push(groupBy); + } + $scope.targetBlur(); + } + $scope.isTagGroupBy = false; + $scope.isValueGroupBy = false; + $scope.isTimeGroupBy = false; + $scope.addGroupByMode = false; + }; + + $scope.removeGroupByTag = function(index) { + $scope.target.groupByTags.splice(index, 1); + if(_.size($scope.target.groupByTags)===0) { + $scope.target.groupByTags = null; + } + $scope.targetBlur(); + }; + + $scope.removeNonTagGroupBy = function(index) { + $scope.target.nonTagGroupBys.splice(index, 1); + if(_.size($scope.target.nonTagGroupBys)===0) { + $scope.target.nonTagGroupBys = null; + } + $scope.targetBlur(); + }; + + $scope.changeGroupByInput = function() { + $scope.isTagGroupBy = $scope.target.currentGroupByType==='tag'; + $scope.isValueGroupBy = $scope.target.currentGroupByType==='value'; + $scope.isTimeGroupBy = $scope.target.currentGroupByType==='time'; + $scope.validateGroupBy(); + }; + + $scope.validateGroupBy = function() { + delete $scope.target.errors.groupBy; + var errors = {}; + $scope.isGroupByValid = true; + if($scope.isTagGroupBy) { + if(!$scope.target.groupBy.tagKey) { + $scope.isGroupByValid = false; + errors.tagKey = 'You must supply a tag name'; + } + } + if($scope.isValueGroupBy) { + if(!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) { + errors.valueRange = "Range must be an integer"; + $scope.isGroupByValid = false; + } + } + if($scope.isTimeGroupBy) { + try { + $scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval); + } catch(err) { + errors.timeInterval = err.message; + $scope.isGroupByValid = false; + } + if(!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) { + errors.groupCount = "Group count must be an integer"; + $scope.isGroupByValid = false; + } + } + + if(!_.isEmpty(errors)) { + $scope.target.errors.groupBy = errors; + } + }; + + function isInt(n) { + return parseInt(n) % 1 === 0; + } + + ////////////////////////////// + // HORIZONTAL AGGREGATION + ////////////////////////////// + + $scope.addHorizontalAggregator = function() { + if (!$scope.addHorizontalAggregatorMode) { + $scope.addHorizontalAggregatorMode = true; + $scope.target.currentHorizontalAggregatorName = 'avg'; + $scope.hasSamplingRate = true; + $scope.validateHorizontalAggregator(); + return; + } + + $scope.validateHorizontalAggregator(); + // nb: if error is found, means that user clicked on cross : cancels input + if(_.isEmpty($scope.target.errors.horAggregator)) { + if (!$scope.target.horizontalAggregators) { + $scope.target.horizontalAggregators = []; + } + var aggregator = { + name:$scope.target.currentHorizontalAggregatorName + }; + if($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;} + if($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;} + if($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;} + if($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;} + $scope.target.horizontalAggregators.push(aggregator); + $scope.targetBlur(); + } + + $scope.addHorizontalAggregatorMode = false; + $scope.hasSamplingRate = false; + $scope.hasUnit = false; + $scope.hasFactor = false; + $scope.hasPercentile = false; + + }; + + $scope.removeHorizontalAggregator = function(index) { + $scope.target.horizontalAggregators.splice(index, 1); + if(_.size($scope.target.horizontalAggregators)===0) { + $scope.target.horizontalAggregators = null; + } + + $scope.targetBlur(); + }; + + $scope.changeHorAggregationInput = function() { + $scope.hasSamplingRate = _.contains(['avg','dev','max','min','sum','least_squares','count','percentile'], + $scope.target.currentHorizontalAggregatorName); + $scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName); + $scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName); + $scope.hasPercentile = 'percentile'===$scope.target.currentHorizontalAggregatorName; + $scope.validateHorizontalAggregator(); + }; + + $scope.validateHorizontalAggregator = function() { + delete $scope.target.errors.horAggregator; + var errors = {}; + $scope.isAggregatorValid = true; + if($scope.hasSamplingRate) { + try { + $scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate); + } catch(err) { + errors.samplingRate = err.message; + $scope.isAggregatorValid = false; + } + } + if($scope.hasFactor) { + if(!$scope.target.horAggregator.factor) { + errors.factor = 'You must supply a numeric value for this aggregator'; + $scope.isAggregatorValid = false; + } + else if(parseInt($scope.target.horAggregator.factor)===0 && $scope.target.currentHorizontalAggregatorName==='div') { + errors.factor = 'Cannot divide by 0'; + $scope.isAggregatorValid = false; + } + } + if($scope.hasPercentile) { + if(!$scope.target.horAggregator.percentile || + $scope.target.horAggregator.percentile<=0 || + $scope.target.horAggregator.percentile>1) { + errors.percentile = 'Percentile must be between 0 and 1'; + $scope.isAggregatorValid = false; + } + } + + if(!_.isEmpty(errors)) { + $scope.target.errors.horAggregator = errors; + } + }; + + $scope.alert = function(message) { + alert(message); + }; + + ////////////////////////////// + // VALIDATION + ////////////////////////////// + + function MetricListToObject(MetricList) { + var result = {}; + var Metric; + var MetricArray = []; + var MetricCnt = 0; + for (var i =0;i < MetricList.length; i++) { + Metric = MetricList[i]; + MetricArray = Metric.split('.'); + if(!result.hasOwnProperty(MetricArray[0])) { + result[MetricArray[0]] = {}; + } + if(!result[MetricArray[0]].hasOwnProperty(MetricArray[1])) { + result[MetricArray[0]][MetricArray[1]] = []; + } + result[MetricArray[0]][MetricArray[1]].push(MetricArray[2]); + } + return result; + } + + function validateTarget(target) { + var errs = {}; + + if (!target.metric) { + errs.metric = "You must supply a metric name."; + } + + try { + if (target.sampling) { + $scope.datasource.convertToKairosInterval(target.sampling); + } + } catch(err) { + errs.sampling = err.message; + } + + return errs; + } + + }); + +}); From 15188c4a885c9f884a867e6711c2461f1a87d033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 29 Mar 2015 20:13:32 +0200 Subject: [PATCH 013/398] Moved kairosdb data source to correct folder --- {src => public}/app/plugins/datasource/kairosdb/datasource.js | 0 .../app/plugins/datasource/kairosdb/partials/config.html | 0 .../app/plugins/datasource/kairosdb/partials/query.editor.html | 0 {src => public}/app/plugins/datasource/kairosdb/plugin.json | 0 {src => public}/app/plugins/datasource/kairosdb/queryCtrl.js | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {src => public}/app/plugins/datasource/kairosdb/datasource.js (100%) rename {src => public}/app/plugins/datasource/kairosdb/partials/config.html (100%) rename {src => public}/app/plugins/datasource/kairosdb/partials/query.editor.html (100%) rename {src => public}/app/plugins/datasource/kairosdb/plugin.json (100%) rename {src => public}/app/plugins/datasource/kairosdb/queryCtrl.js (100%) diff --git a/src/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js similarity index 100% rename from src/app/plugins/datasource/kairosdb/datasource.js rename to public/app/plugins/datasource/kairosdb/datasource.js diff --git a/src/app/plugins/datasource/kairosdb/partials/config.html b/public/app/plugins/datasource/kairosdb/partials/config.html similarity index 100% rename from src/app/plugins/datasource/kairosdb/partials/config.html rename to public/app/plugins/datasource/kairosdb/partials/config.html diff --git a/src/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html similarity index 100% rename from src/app/plugins/datasource/kairosdb/partials/query.editor.html rename to public/app/plugins/datasource/kairosdb/partials/query.editor.html diff --git a/src/app/plugins/datasource/kairosdb/plugin.json b/public/app/plugins/datasource/kairosdb/plugin.json similarity index 100% rename from src/app/plugins/datasource/kairosdb/plugin.json rename to public/app/plugins/datasource/kairosdb/plugin.json diff --git a/src/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js similarity index 100% rename from src/app/plugins/datasource/kairosdb/queryCtrl.js rename to public/app/plugins/datasource/kairosdb/queryCtrl.js From 795cee13c8b011c8e2ccbf7b0fe12a83de699503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 29 Mar 2015 20:30:42 +0200 Subject: [PATCH 014/398] KairosDB data source plugin is messy, needs a lot of clean up & refactoring, please help --- .../plugins/datasource/kairosdb/datasource.js | 2 +- .../plugins/datasource/kairosdb/queryCtrl.js | 26 ++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index e055c118956..bcce19cb83f 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -217,7 +217,7 @@ function (angular, _, kbn) { if(chosenAggregator.sampling_rate) { returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); returnedAggregator.align_sampling = true; - returnedAggregator.align_start_time=true; + returnedAggregator.align_start_time =true; } if(chosenAggregator.unit) { returnedAggregator.unit = chosenAggregator.unit+'s'; diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index fef1e4d39f4..73321d3016a 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -53,30 +53,26 @@ function (angular, _) { _.move($scope.panel.targets, fromIndex, toIndex); }; - ////////////////////////////// - // SUGGESTION QUERIES - ////////////////////////////// - + // Fetch metric list $scope.updateMetricList = function() { $scope.metricListLoading = true; metricList = []; $scope.datasource.performMetricSuggestQuery().then(function(series) { metricList = series; $scope.metric.list = series; - if ($scope.target.metric) + if ($scope.target.metric) { $scope.metric.value = $scope.target.metric; - else + } + else { $scope.metric.value = ""; + } $scope.metricListLoading = false; return metricList; }); }; $scope.suggestTagKeys = function(query, callback) { - $scope.updateTimeRange(); - callback($scope.datasource - .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'key','')); - + callback($scope.datasource.performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'key','')); }; $scope.suggestTagValues = function(query, callback) { @@ -84,10 +80,7 @@ function (angular, _) { .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'value',$scope.target.currentTagKey)); }; - ////////////////////////////// - // FILTER by TAG - ////////////////////////////// - + // Filter metric by tag $scope.addFilterTag = function() { if (!$scope.addFilterTagMode) { $scope.addFilterTagMode = true; @@ -333,10 +326,7 @@ function (angular, _) { alert(message); }; - ////////////////////////////// - // VALIDATION - ////////////////////////////// - + // Validation function MetricListToObject(MetricList) { var result = {}; var Metric; From 66f5411402f87d7ba4ca34e96b83dd97601f0e19 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 18:39:41 +0900 Subject: [PATCH 015/398] Fix class names in query.editor.html of KairosDB Plugin --- .../kairosdb/partials/query.editor.html | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html index 8a794e7fdf2..23ddecabc4e 100644 --- a/public/app/plugins/datasource/kairosdb/partials/query.editor.html +++ b/public/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -10,7 +10,7 @@
  • - +
  • @@ -98,13 +98,13 @@
  • {{key}} = {{value}} - +
  • - +
  • @@ -130,12 +130,12 @@ - +
  • - - + +
  • @@ -152,7 +152,7 @@
  • {{key}} - +
  • @@ -163,13 +163,13 @@
  • {{_.values(groupByObject)}} - +
  • - +
  • @@ -190,7 +190,7 @@ - +
  • @@ -204,7 +204,7 @@ - +
  • @@ -219,7 +219,7 @@ - +
  • @@ -233,13 +233,13 @@ - +
  • - - + +
  • @@ -285,7 +285,7 @@ - + @@ -375,7 +375,7 @@ - + From ed69ddedbf2a58a2ae24a1bf11320983624fe8c4 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 19:46:19 +0900 Subject: [PATCH 016/398] Fix styles warned by jscs --- public/app/plugins/datasource/kairosdb/datasource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index bcce19cb83f..7376c528ce9 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -27,7 +27,7 @@ function (angular, _, kbn) { var end = options.range.to; var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options))); - var plotParams = _.compact(_.map(options.targets, function(target){ + var plotParams = _.compact(_.map(options.targets, function(target) { var alias = target.alias; if (typeof target.alias == 'undefined' || target.alias == "") alias = target.metric; From ae2201ef6f50dd79ae4165c6e969df6d93e59d09 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 20:03:43 +0900 Subject: [PATCH 017/398] Add space after keywords --- .../plugins/datasource/kairosdb/datasource.js | 54 +++++++-------- .../plugins/datasource/kairosdb/queryCtrl.js | 68 +++++++++---------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index 7376c528ce9..ecd4564f72f 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -86,7 +86,7 @@ function (angular, _, kbn) { }; KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname,range,type,keyValue) { - if(tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && + if (tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && (range.to === tagList.range.to)) { return getTagListFromResponse(tagList.results,type,keyValue); } @@ -119,10 +119,10 @@ function (angular, _, kbn) { if (!results.data) { return []; } - if(type==="key") { + if (type==="key") { return _.keys(results.data.queries[0].results[0].tags); } - else if(type==="value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { + else if (type==="value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { return results.data.queries[0].results[0].tags[keyValue]; } return []; @@ -134,13 +134,13 @@ function (angular, _, kbn) { * @returns {*} */ function handleQueryError(results) { - if(results.data.errors && !_.isEmpty(results.data.errors)) { + if (results.data.errors && !_.isEmpty(results.data.errors)) { var errors = { message: results.data.errors[0] }; return $q.reject(errors); } - else{ + else { return $q.reject(results); } } @@ -158,15 +158,15 @@ function (angular, _, kbn) { var target = plotParams[index].alias; var details = " ( "; _.each(result.group_by,function(element) { - if(element.name==="tag") { + if (element.name==="tag") { _.each(element.group,function(value, key) { details+= key+"="+value+" "; }); } - else if(element.name==="value") { + else if (element.name==="value") { details+= 'value_group='+element.group.group_number+" "; } - else if(element.name==="time") { + else if (element.name==="time") { details+= 'time_group='+element.group.group_number+" "; } }); @@ -201,7 +201,7 @@ function (angular, _, kbn) { }; query.aggregators = []; - if(target.downsampling!=='(NONE)') { + if (target.downsampling!=='(NONE)') { query.aggregators.push({ name: target.downsampling, align_sampling: true, @@ -209,46 +209,46 @@ function (angular, _, kbn) { sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval) }); } - if(target.horizontalAggregators) { + if (target.horizontalAggregators) { _.each(target.horizontalAggregators,function(chosenAggregator) { var returnedAggregator = { name:chosenAggregator.name }; - if(chosenAggregator.sampling_rate) { + if (chosenAggregator.sampling_rate) { returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); returnedAggregator.align_sampling = true; returnedAggregator.align_start_time =true; } - if(chosenAggregator.unit) { + if (chosenAggregator.unit) { returnedAggregator.unit = chosenAggregator.unit+'s'; } - if(chosenAggregator.factor && chosenAggregator.name==='div') { + if (chosenAggregator.factor && chosenAggregator.name==='div') { returnedAggregator.divisor = chosenAggregator.factor; } - else if(chosenAggregator.factor && chosenAggregator.name==='scale') { + else if (chosenAggregator.factor && chosenAggregator.name==='scale') { returnedAggregator.factor = chosenAggregator.factor; } - if(chosenAggregator.percentile) { + if (chosenAggregator.percentile) { returnedAggregator.percentile = chosenAggregator.percentile; } query.aggregators.push(returnedAggregator); }); } - if(_.isEmpty(query.aggregators)) { + if (_.isEmpty(query.aggregators)) { delete query.aggregators; } - if(target.tags) { + if (target.tags) { query.tags = angular.copy(target.tags); } - if(target.groupByTags || target.nonTagGroupBys) { + if (target.groupByTags || target.nonTagGroupBys) { query.group_by = []; - if(target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});} - if(target.nonTagGroupBys) { + if (target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});} + if (target.nonTagGroupBys) { _.each(target.nonTagGroupBys,function(rawGroupBy) { var formattedGroupBy = angular.copy(rawGroupBy); - if(formattedGroupBy.name==='time') { + if (formattedGroupBy.name==='time') { formattedGroupBy.range_size=KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); } query.group_by.push(formattedGroupBy); @@ -266,7 +266,7 @@ function (angular, _, kbn) { var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/; var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/; var matches = intervalString.match(interval_regex_ms); - if(!matches) { + if (!matches) { matches = intervalString.match(interval_regex); } if (!matches) { @@ -276,12 +276,12 @@ function (angular, _, kbn) { var value = matches[1]; var unit = matches[2]; if (value%1!==0) { - if(unit==='ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} + if (unit==='ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} value = Math.round(kbn.intervals_in_seconds[unit]*value*1000); unit = 'ms'; } - switch(unit) { + switch (unit) { case 'ms': unit = 'milliseconds'; break; @@ -334,7 +334,7 @@ function (angular, _, kbn) { if (result) { var value = result[1]; var unit = result[2]; - switch(unit) { + switch (unit) { case 'ms': unit = 'milliseconds'; break; @@ -375,7 +375,7 @@ function (angular, _, kbn) { date = kbn.parseDate(date); } - if(_.isDate(date)) { + if (_.isDate(date)) { name = start_stop_name + "_absolute"; response_obj[name] = date.getTime(); return; @@ -403,7 +403,7 @@ function (angular, _, kbn) { if (prvDelta >= limit && nxtDelta < limit) datapoints[LastIndx][0] = datapoints[LastIndx - 1][0]; - for (var i = 1; i < arrLength - 1; i++){ + for (var i = 1; i < arrLength - 1; i++) { prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); if (prvDelta >= limit && nxtDelta >= limit) diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 73321d3016a..bb0f74a68b3 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -94,7 +94,7 @@ function (angular, _) { $scope.validateFilterTag(); if (!$scope.target.errors.tags) { - if(!_.has($scope.target.tags,$scope.target.currentTagKey)) { + if (!_.has($scope.target.tags,$scope.target.currentTagKey)) { $scope.target.tags[$scope.target.currentTagKey] = []; } $scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue); @@ -108,7 +108,7 @@ function (angular, _) { $scope.removeFilterTag = function(key) { delete $scope.target.tags[key]; - if(_.size($scope.target.tags)===0) { + if (_.size($scope.target.tags)===0) { $scope.target.tags = null; } $scope.targetBlur(); @@ -116,7 +116,7 @@ function (angular, _) { $scope.validateFilterTag = function() { $scope.target.errors.tags = null; - if(!$scope.target.currentTagKey || !$scope.target.currentTagValue) { + if (!$scope.target.currentTagKey || !$scope.target.currentTagValue) { $scope.target.errors.tags = "You must specify a tag name and value."; } }; @@ -136,7 +136,7 @@ function (angular, _) { $scope.validateGroupBy(); // nb: if error is found, means that user clicked on cross : cancels input if (_.isEmpty($scope.target.errors.groupBy)) { - if($scope.isTagGroupBy) { + if ($scope.isTagGroupBy) { if (!$scope.target.groupByTags) { $scope.target.groupByTags = []; } @@ -147,15 +147,15 @@ function (angular, _) { } $scope.target.groupBy.tagKey = ''; } - else { + else { if (!$scope.target.nonTagGroupBys) { $scope.target.nonTagGroupBys = []; } var groupBy = { name: $scope.target.currentGroupByType }; - if($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;} - else if($scope.isTimeGroupBy) { + if ($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;} + else if ($scope.isTimeGroupBy) { groupBy.range_size = $scope.target.groupBy.timeInterval; groupBy.group_count = $scope.target.groupBy.groupCount; } @@ -171,7 +171,7 @@ function (angular, _) { $scope.removeGroupByTag = function(index) { $scope.target.groupByTags.splice(index, 1); - if(_.size($scope.target.groupByTags)===0) { + if (_.size($scope.target.groupByTags)===0) { $scope.target.groupByTags = null; } $scope.targetBlur(); @@ -179,7 +179,7 @@ function (angular, _) { $scope.removeNonTagGroupBy = function(index) { $scope.target.nonTagGroupBys.splice(index, 1); - if(_.size($scope.target.nonTagGroupBys)===0) { + if (_.size($scope.target.nonTagGroupBys)===0) { $scope.target.nonTagGroupBys = null; } $scope.targetBlur(); @@ -196,32 +196,32 @@ function (angular, _) { delete $scope.target.errors.groupBy; var errors = {}; $scope.isGroupByValid = true; - if($scope.isTagGroupBy) { - if(!$scope.target.groupBy.tagKey) { + if ($scope.isTagGroupBy) { + if (!$scope.target.groupBy.tagKey) { $scope.isGroupByValid = false; errors.tagKey = 'You must supply a tag name'; } } - if($scope.isValueGroupBy) { - if(!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) { + if ($scope.isValueGroupBy) { + if (!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) { errors.valueRange = "Range must be an integer"; $scope.isGroupByValid = false; } } - if($scope.isTimeGroupBy) { + if ($scope.isTimeGroupBy) { try { $scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval); - } catch(err) { + } catch (err) { errors.timeInterval = err.message; $scope.isGroupByValid = false; } - if(!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) { + if (!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) { errors.groupCount = "Group count must be an integer"; $scope.isGroupByValid = false; } } - if(!_.isEmpty(errors)) { + if (!_.isEmpty(errors)) { $scope.target.errors.groupBy = errors; } }; @@ -245,17 +245,17 @@ function (angular, _) { $scope.validateHorizontalAggregator(); // nb: if error is found, means that user clicked on cross : cancels input - if(_.isEmpty($scope.target.errors.horAggregator)) { + if (_.isEmpty($scope.target.errors.horAggregator)) { if (!$scope.target.horizontalAggregators) { $scope.target.horizontalAggregators = []; } var aggregator = { name:$scope.target.currentHorizontalAggregatorName }; - if($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;} - if($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;} - if($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;} - if($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;} + if ($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;} + if ($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;} + if ($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;} + if ($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;} $scope.target.horizontalAggregators.push(aggregator); $scope.targetBlur(); } @@ -270,7 +270,7 @@ function (angular, _) { $scope.removeHorizontalAggregator = function(index) { $scope.target.horizontalAggregators.splice(index, 1); - if(_.size($scope.target.horizontalAggregators)===0) { + if (_.size($scope.target.horizontalAggregators)===0) { $scope.target.horizontalAggregators = null; } @@ -290,26 +290,26 @@ function (angular, _) { delete $scope.target.errors.horAggregator; var errors = {}; $scope.isAggregatorValid = true; - if($scope.hasSamplingRate) { + if ($scope.hasSamplingRate) { try { $scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate); - } catch(err) { + } catch (err) { errors.samplingRate = err.message; $scope.isAggregatorValid = false; } } - if($scope.hasFactor) { - if(!$scope.target.horAggregator.factor) { + if ($scope.hasFactor) { + if (!$scope.target.horAggregator.factor) { errors.factor = 'You must supply a numeric value for this aggregator'; $scope.isAggregatorValid = false; } - else if(parseInt($scope.target.horAggregator.factor)===0 && $scope.target.currentHorizontalAggregatorName==='div') { + else if (parseInt($scope.target.horAggregator.factor)===0 && $scope.target.currentHorizontalAggregatorName==='div') { errors.factor = 'Cannot divide by 0'; $scope.isAggregatorValid = false; } } - if($scope.hasPercentile) { - if(!$scope.target.horAggregator.percentile || + if ($scope.hasPercentile) { + if (!$scope.target.horAggregator.percentile || $scope.target.horAggregator.percentile<=0 || $scope.target.horAggregator.percentile>1) { errors.percentile = 'Percentile must be between 0 and 1'; @@ -317,7 +317,7 @@ function (angular, _) { } } - if(!_.isEmpty(errors)) { + if (!_.isEmpty(errors)) { $scope.target.errors.horAggregator = errors; } }; @@ -335,10 +335,10 @@ function (angular, _) { for (var i =0;i < MetricList.length; i++) { Metric = MetricList[i]; MetricArray = Metric.split('.'); - if(!result.hasOwnProperty(MetricArray[0])) { + if (!result.hasOwnProperty(MetricArray[0])) { result[MetricArray[0]] = {}; } - if(!result[MetricArray[0]].hasOwnProperty(MetricArray[1])) { + if (!result[MetricArray[0]].hasOwnProperty(MetricArray[1])) { result[MetricArray[0]][MetricArray[1]] = []; } result[MetricArray[0]][MetricArray[1]].push(MetricArray[2]); @@ -357,7 +357,7 @@ function (angular, _) { if (target.sampling) { $scope.datasource.convertToKairosInterval(target.sampling); } - } catch(err) { + } catch (err) { errs.sampling = err.message; } From 2b5fa599fbcffad8e938a5574bf8257a16f86566 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 20:11:42 +0900 Subject: [PATCH 018/398] Fix styles warned by jshint --- .../plugins/datasource/kairosdb/datasource.js | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index ecd4564f72f..086b07b88e6 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -29,8 +29,9 @@ function (angular, _, kbn) { var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options))); var plotParams = _.compact(_.map(options.targets, function(target) { var alias = target.alias; - if (typeof target.alias == 'undefined' || target.alias == "") + if (typeof target.alias === 'undefined' || target.alias === "") { alias = target.metric; + } return !target.hide ? {alias: alias, exouter: target.exOuter} @@ -171,8 +172,9 @@ function (angular, _, kbn) { } }); details+= ") "; - if (details != " ( ) ") + if (details !== " ( ) ") { target += details; + } var datapoints = []; for (var i = 0; i < result.values.length; i++) { @@ -180,8 +182,9 @@ function (angular, _, kbn) { var v = result.values[i][1]; datapoints[i] = [v, t]; } - if (plotParams[index].exouter) - datapoints = PeakFilter(datapoints, 10); + if (plotParams[index].exouter) { + datapoints = new PeakFilter(datapoints, 10); + } output.push({ target: target, datapoints: datapoints }); }); index ++; @@ -387,27 +390,31 @@ function (angular, _, kbn) { function PeakFilter(dataIn, limit) { var datapoints = dataIn; var arrLength = datapoints.length; - if (arrLength <= 3) + if (arrLength <= 3) { return datapoints; + } var LastIndx = arrLength - 1; // Check first point var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]); var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]); - if (prvDelta >= limit && nxtDelta < limit) + if (prvDelta >= limit && nxtDelta < limit) { datapoints[0][0] = datapoints[1][0]; + } // Check last point prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]); nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]); - if (prvDelta >= limit && nxtDelta < limit) + if (prvDelta >= limit && nxtDelta < limit) { datapoints[LastIndx][0] = datapoints[LastIndx - 1][0]; + } for (var i = 1; i < arrLength - 1; i++) { prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); - if (prvDelta >= limit && nxtDelta >= limit) + if (prvDelta >= limit && nxtDelta >= limit) { datapoints[i][0] = (datapoints[i-1][0] + datapoints[i+1][0]) / 2; + } } return datapoints; From ddb4b928a06c531f79576d85272e51173575b901 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 20:18:01 +0900 Subject: [PATCH 019/398] Delete MetricListToObject which never used --- .../plugins/datasource/kairosdb/queryCtrl.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index bb0f74a68b3..20b77c59644 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -327,25 +327,6 @@ function (angular, _) { }; // Validation - function MetricListToObject(MetricList) { - var result = {}; - var Metric; - var MetricArray = []; - var MetricCnt = 0; - for (var i =0;i < MetricList.length; i++) { - Metric = MetricList[i]; - MetricArray = Metric.split('.'); - if (!result.hasOwnProperty(MetricArray[0])) { - result[MetricArray[0]] = {}; - } - if (!result[MetricArray[0]].hasOwnProperty(MetricArray[1])) { - result[MetricArray[0]][MetricArray[1]] = []; - } - result[MetricArray[0]][MetricArray[1]].push(MetricArray[2]); - } - return result; - } - function validateTarget(target) { var errs = {}; From a2b751976e7472ce0ab0ce6e303d7285bc584ec4 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 20:41:09 +0900 Subject: [PATCH 020/398] Add space before and after binary operators --- .../plugins/datasource/kairosdb/datasource.js | 44 +++++++++---------- .../plugins/datasource/kairosdb/queryCtrl.js | 18 ++++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index 086b07b88e6..cc1ee79c852 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -55,7 +55,7 @@ function (angular, _, kbn) { var reqBody = { metrics: queries }; - reqBody.cache_time=0; + reqBody.cache_time = 0; convertToKairosTime(start,reqBody,'start'); convertToKairosTime(end,reqBody,'end'); var options = { @@ -120,10 +120,10 @@ function (angular, _, kbn) { if (!results.data) { return []; } - if (type==="key") { + if (type === "key") { return _.keys(results.data.queries[0].results[0].tags); } - else if (type==="value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { + else if (type === "value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { return results.data.queries[0].results[0].tags[keyValue]; } return []; @@ -159,19 +159,19 @@ function (angular, _, kbn) { var target = plotParams[index].alias; var details = " ( "; _.each(result.group_by,function(element) { - if (element.name==="tag") { + if (element.name === "tag") { _.each(element.group,function(value, key) { - details+= key+"="+value+" "; + details += key + "=" + value + " "; }); } - else if (element.name==="value") { - details+= 'value_group='+element.group.group_number+" "; + else if (element.name === "value") { + details += 'value_group=' + element.group.group_number + " "; } - else if (element.name==="time") { - details+= 'time_group='+element.group.group_number+" "; + else if (element.name === "time") { + details += 'time_group=' + element.group.group_number + " "; } }); - details+= ") "; + details += ") "; if (details !== " ( ) ") { target += details; } @@ -204,7 +204,7 @@ function (angular, _, kbn) { }; query.aggregators = []; - if (target.downsampling!=='(NONE)') { + if (target.downsampling !== '(NONE)') { query.aggregators.push({ name: target.downsampling, align_sampling: true, @@ -223,13 +223,13 @@ function (angular, _, kbn) { returnedAggregator.align_start_time =true; } if (chosenAggregator.unit) { - returnedAggregator.unit = chosenAggregator.unit+'s'; + returnedAggregator.unit = chosenAggregator.unit + 's'; } - if (chosenAggregator.factor && chosenAggregator.name==='div') { + if (chosenAggregator.factor && chosenAggregator.name === 'div') { returnedAggregator.divisor = chosenAggregator.factor; } - else if (chosenAggregator.factor && chosenAggregator.name==='scale') { - returnedAggregator.factor = chosenAggregator.factor; + else if (chosenAggregator.factor && chosenAggregator.name === 'scale') { + returnedAggregator.factor = chosenAggregator.factor; } if (chosenAggregator.percentile) { returnedAggregator.percentile = chosenAggregator.percentile; @@ -249,10 +249,10 @@ function (angular, _, kbn) { query.group_by = []; if (target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});} if (target.nonTagGroupBys) { - _.each(target.nonTagGroupBys,function(rawGroupBy) { + _.each(target.nonTagGroupBys, function(rawGroupBy) { var formattedGroupBy = angular.copy(rawGroupBy); - if (formattedGroupBy.name==='time') { - formattedGroupBy.range_size=KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); + if (formattedGroupBy.name === 'time') { + formattedGroupBy.range_size = KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); } query.group_by.push(formattedGroupBy); }); @@ -278,9 +278,9 @@ function (angular, _, kbn) { var value = matches[1]; var unit = matches[2]; - if (value%1!==0) { - if (unit==='ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} - value = Math.round(kbn.intervals_in_seconds[unit]*value*1000); + if (value%1 !== 0) { + if (unit === 'ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} + value = Math.round(kbn.intervals_in_seconds[unit] * value * 1000); unit = 'ms'; } @@ -413,7 +413,7 @@ function (angular, _, kbn) { prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); if (prvDelta >= limit && nxtDelta >= limit) { - datapoints[i][0] = (datapoints[i-1][0] + datapoints[i+1][0]) / 2; + datapoints[i][0] = (datapoints[i - 1][0] + datapoints[i + 1][0]) / 2; } } diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 20b77c59644..0de3c953152 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -108,7 +108,7 @@ function (angular, _) { $scope.removeFilterTag = function(key) { delete $scope.target.tags[key]; - if (_.size($scope.target.tags)===0) { + if (_.size($scope.target.tags) === 0) { $scope.target.tags = null; } $scope.targetBlur(); @@ -171,7 +171,7 @@ function (angular, _) { $scope.removeGroupByTag = function(index) { $scope.target.groupByTags.splice(index, 1); - if (_.size($scope.target.groupByTags)===0) { + if (_.size($scope.target.groupByTags) === 0) { $scope.target.groupByTags = null; } $scope.targetBlur(); @@ -179,16 +179,16 @@ function (angular, _) { $scope.removeNonTagGroupBy = function(index) { $scope.target.nonTagGroupBys.splice(index, 1); - if (_.size($scope.target.nonTagGroupBys)===0) { + if (_.size($scope.target.nonTagGroupBys) === 0) { $scope.target.nonTagGroupBys = null; } $scope.targetBlur(); }; $scope.changeGroupByInput = function() { - $scope.isTagGroupBy = $scope.target.currentGroupByType==='tag'; - $scope.isValueGroupBy = $scope.target.currentGroupByType==='value'; - $scope.isTimeGroupBy = $scope.target.currentGroupByType==='time'; + $scope.isTagGroupBy = $scope.target.currentGroupByType === 'tag'; + $scope.isValueGroupBy = $scope.target.currentGroupByType === 'value'; + $scope.isTimeGroupBy = $scope.target.currentGroupByType === 'time'; $scope.validateGroupBy(); }; @@ -270,7 +270,7 @@ function (angular, _) { $scope.removeHorizontalAggregator = function(index) { $scope.target.horizontalAggregators.splice(index, 1); - if (_.size($scope.target.horizontalAggregators)===0) { + if (_.size($scope.target.horizontalAggregators) === 0) { $scope.target.horizontalAggregators = null; } @@ -282,7 +282,7 @@ function (angular, _) { $scope.target.currentHorizontalAggregatorName); $scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName); $scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName); - $scope.hasPercentile = 'percentile'===$scope.target.currentHorizontalAggregatorName; + $scope.hasPercentile = 'percentile' === $scope.target.currentHorizontalAggregatorName; $scope.validateHorizontalAggregator(); }; @@ -303,7 +303,7 @@ function (angular, _) { errors.factor = 'You must supply a numeric value for this aggregator'; $scope.isAggregatorValid = false; } - else if (parseInt($scope.target.horAggregator.factor)===0 && $scope.target.currentHorizontalAggregatorName==='div') { + else if (parseInt($scope.target.horAggregator.factor) === 0 && $scope.target.currentHorizontalAggregatorName === 'div') { errors.factor = 'Cannot divide by 0'; $scope.isAggregatorValid = false; } From 88bf0cdb9e6a734e30d9355f64f69fd30a5d35b9 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 12 Apr 2015 20:51:35 +0900 Subject: [PATCH 021/398] Fix styles around 'function' --- public/app/plugins/datasource/kairosdb/datasource.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index cc1ee79c852..a93febf9ff4 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -149,18 +149,18 @@ function (angular, _, kbn) { function handleKairosDBQueryResponse(plotParams, results) { var output = []; var index = 0; - _.each(results.data.queries, function (series) { + _.each(results.data.queries, function(series) { var sample_size = series.sample_size; console.log("sample_size:" + sample_size + " samples"); - _.each(series.results, function (result) { + _.each(series.results, function(result) { //var target = result.name; var target = plotParams[index].alias; var details = " ( "; - _.each(result.group_by,function(element) { + _.each(result.group_by, function(element) { if (element.name === "tag") { - _.each(element.group,function(value, key) { + _.each(element.group, function(value, key) { details += key + "=" + value + " "; }); } @@ -213,7 +213,7 @@ function (angular, _, kbn) { }); } if (target.horizontalAggregators) { - _.each(target.horizontalAggregators,function(chosenAggregator) { + _.each(target.horizontalAggregators, function(chosenAggregator) { var returnedAggregator = { name:chosenAggregator.name }; From 76b517b361235e9478eb84d994be74e0761b5a49 Mon Sep 17 00:00:00 2001 From: William Wei Date: Fri, 17 Apr 2015 15:51:26 +0800 Subject: [PATCH 022/398] allow graphite metrics name contain '~' when a metrics name contains '~', id does not impact graph display. but you can not use grafana UI to edit metrics with realtime graphite query. --- public/app/plugins/datasource/graphite/lexer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/graphite/lexer.js b/public/app/plugins/datasource/graphite/lexer.js index 7306737d96e..ffb65121a60 100644 --- a/public/app/plugins/datasource/graphite/lexer.js +++ b/public/app/plugins/datasource/graphite/lexer.js @@ -119,6 +119,7 @@ define([ identifierStartTable[i] = i >= 48 && i <= 57 || // 0-9 i === 36 || // $ + i === 126 || // ~ i >= 65 && i <= 90 || // A-Z i === 95 || // _ i === 45 || // - From d57ffad5e176f938c7d48670c5d7b4b7c4a956a1 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 19 Apr 2015 21:39:23 +0900 Subject: [PATCH 023/398] Add a space between arguments --- .../plugins/datasource/kairosdb/datasource.js | 22 +++++++++---------- .../plugins/datasource/kairosdb/queryCtrl.js | 6 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index a93febf9ff4..5fc25590dbd 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -44,7 +44,7 @@ function (angular, _, kbn) { d.resolve({ data: [] }); return d.promise; } - return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias,handleQueryError); + return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias, handleQueryError); }; /////////////////////////////////////////////////////////////////////// @@ -56,8 +56,8 @@ function (angular, _, kbn) { metrics: queries }; reqBody.cache_time = 0; - convertToKairosTime(start,reqBody,'start'); - convertToKairosTime(end,reqBody,'end'); + convertToKairosTime(start, reqBody, 'start'); + convertToKairosTime(end, reqBody, 'end'); var options = { method: 'POST', url: '/api/v1/datapoints/query', @@ -86,10 +86,10 @@ function (angular, _, kbn) { }; - KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname,range,type,keyValue) { + KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname, range, type, keyValue) { if (tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && (range.to === tagList.range.to)) { - return getTagListFromResponse(tagList.results,type,keyValue); + return getTagListFromResponse(tagList.results, type, keyValue); } tagList = { metricName:metricname, @@ -98,8 +98,8 @@ function (angular, _, kbn) { var body = { metrics : [{name : metricname}] }; - convertToKairosTime(range.from,body,'start'); - convertToKairosTime(range.to,body,'end'); + convertToKairosTime(range.from, body, 'start'); + convertToKairosTime(range.to, body, 'end'); var options = { url : this.url + '/api/v1/datapoints/query/tags', method : 'POST', @@ -107,7 +107,7 @@ function (angular, _, kbn) { }; return $http(options).then(function(results) { tagList.results = results; - return getTagListFromResponse(results,type,keyValue); + return getTagListFromResponse(results, type, keyValue); }); }; @@ -116,14 +116,14 @@ function (angular, _, kbn) { /// Formatting methods //////////////////////////////////////////////////////////////////////// - function getTagListFromResponse(results,type,keyValue) { + function getTagListFromResponse(results, type, keyValue) { if (!results.data) { return []; } if (type === "key") { return _.keys(results.data.queries[0].results[0].tags); } - else if (type === "value" && _.has(results.data.queries[0].results[0].tags,keyValue)) { + else if (type === "value" && _.has(results.data.queries[0].results[0].tags, keyValue)) { return results.data.queries[0].results[0].tags[keyValue]; } return []; @@ -194,7 +194,7 @@ function (angular, _, kbn) { return output2; } - function convertTargetToQuery(options,target) { + function convertTargetToQuery(options, target) { if (!target.metric || target.hide) { return null; } diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 0de3c953152..30c658629b9 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -72,12 +72,12 @@ function (angular, _) { }; $scope.suggestTagKeys = function(query, callback) { - callback($scope.datasource.performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'key','')); + callback($scope.datasource.performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'key', '')); }; $scope.suggestTagValues = function(query, callback) { callback($scope.datasource - .performTagSuggestQuery($scope.target.metric,$scope.rangeUnparsed, 'value',$scope.target.currentTagKey)); + .performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'value', $scope.target.currentTagKey)); }; // Filter metric by tag @@ -94,7 +94,7 @@ function (angular, _) { $scope.validateFilterTag(); if (!$scope.target.errors.tags) { - if (!_.has($scope.target.tags,$scope.target.currentTagKey)) { + if (!_.has($scope.target.tags, $scope.target.currentTagKey)) { $scope.target.tags[$scope.target.currentTagKey] = []; } $scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue); From 05c27d8340e518ef52f1ada15e810c131c5935b1 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 19 Apr 2015 21:52:04 +0900 Subject: [PATCH 024/398] Rename 'KairosDBTargetCtrl' to 'KairosDBQueryCtrl' --- .../plugins/datasource/kairosdb/partials/query.editor.html | 4 ++-- public/app/plugins/datasource/kairosdb/queryCtrl.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html index 23ddecabc4e..dc272ebbf4c 100644 --- a/public/app/plugins/datasource/kairosdb/partials/query.editor.html +++ b/public/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -2,7 +2,7 @@
    @@ -345,7 +345,7 @@
    -
    +
    • diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 30c658629b9..9aad7c41fc9 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -9,7 +9,7 @@ function (angular, _) { var metricList = null; var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; - module.controller('KairosDBTargetCtrl', function($scope) { + module.controller('KairosDBQueryCtrl', function($scope) { $scope.init = function() { $scope.metric = { From dbc07827cfa206aefff43b6211f39e04bed2eb99 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 19 Apr 2015 23:40:48 +0900 Subject: [PATCH 025/398] Add a basic test of KairosDBDatasource --- .../test/specs/kairosdb-datasource-specs.js | 63 +++++++++++++++++++ public/test/test-main.js | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 public/test/specs/kairosdb-datasource-specs.js diff --git a/public/test/specs/kairosdb-datasource-specs.js b/public/test/specs/kairosdb-datasource-specs.js new file mode 100644 index 00000000000..43004656573 --- /dev/null +++ b/public/test/specs/kairosdb-datasource-specs.js @@ -0,0 +1,63 @@ +define([ + 'helpers', + 'plugins/datasource/kairosdb/datasource' +], function(helpers) { + 'use strict'; + + describe('KairosDBDatasource', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['templateSrv'])); + beforeEach(ctx.createService('KairosDBDatasource')); + beforeEach(function() { + ctx.ds = new ctx.service({ url: ''}); + }); + + describe('When querying kairosdb with one target using query editor target spec', function() { + var results; + var urlExpected = "/api/v1/datapoints/query"; + var bodyExpected = { + metrics: [{ name: "test" }], + cache_time: 0, + start_relative: { + value: "1", + unit: "hours" + } + }; + + var query = { + range: { from: 'now-1h', to: 'now' }, + targets: [{ metric: 'test', downsampling: '(NONE)'}] + }; + + var response = { + queries: [{ + sample_size: 60, + results: [{ + name: "test", + values: [[1420070400000, 1]] + }] + }] + }; + + beforeEach(function() { + ctx.$httpBackend.expect('POST', urlExpected, bodyExpected).respond(response); + ctx.ds.query(query).then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + }); + + it('should generate the correct query', function() { + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + + it('should return series list', function() { + expect(results.data.length).to.be(1); + expect(results.data[0].target).to.be('test'); + }); + + }); + + }); + +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index a38f8dca32b..b6fca85e9e5 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -128,6 +128,7 @@ require([ 'specs/influxQueryBuilder-specs', 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', + 'specs/kairosdb-datasource-specs', 'specs/graph-ctrl-specs', 'specs/graph-specs', 'specs/graph-tooltip-specs', @@ -150,4 +151,3 @@ require([ window.__karma__.start(); }); }); - From cc9d2fc139bdf4a9846fdd9b3c113e91aba3804b Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 19 Apr 2015 23:51:48 +0900 Subject: [PATCH 026/398] Suppress LOG in test --- public/app/plugins/datasource/kairosdb/datasource.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index 5fc25590dbd..da3f8ca665f 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -150,9 +150,6 @@ function (angular, _, kbn) { var output = []; var index = 0; _.each(results.data.queries, function(series) { - var sample_size = series.sample_size; - console.log("sample_size:" + sample_size + " samples"); - _.each(series.results, function(result) { //var target = result.name; From c762ad8db2185983f2c6d2c362abbb3f3284827d Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Mon, 20 Apr 2015 00:49:16 +0900 Subject: [PATCH 027/398] Refactoring KairosDB Plugin --- .../plugins/datasource/kairosdb/datasource.js | 171 ++++++++---------- .../plugins/datasource/kairosdb/queryCtrl.js | 11 +- 2 files changed, 88 insertions(+), 94 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index da3f8ca665f..9d25b694891 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -32,18 +32,24 @@ function (angular, _, kbn) { if (typeof target.alias === 'undefined' || target.alias === "") { alias = target.metric; } - return !target.hide - ? {alias: alias, - exouter: target.exOuter} - : null; + + if (!target.hide) { + return { alias: alias, exouter: target.exOuter }; + } + else { + return null; + } })); + var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams); + // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { var d = $q.defer(); d.resolve({ data: [] }); return d.promise; } + return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias, handleQueryError); }; @@ -53,18 +59,19 @@ function (angular, _, kbn) { KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { var reqBody = { - metrics: queries + metrics: queries, + cache_time: 0 }; - reqBody.cache_time = 0; + convertToKairosTime(start, reqBody, 'start'); convertToKairosTime(end, reqBody, 'end'); + var options = { method: 'POST', - url: '/api/v1/datapoints/query', + url: this.url + '/api/v1/datapoints/query', data: reqBody }; - options.url = this.url + options.url; return $http(options); }; @@ -77,39 +84,41 @@ function (angular, _, kbn) { url : this.url + '/api/v1/metricnames', method : 'GET' }; + return $http(options).then(function(results) { if (!results.data) { return []; } return results.data.results; }); - }; KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname, range, type, keyValue) { if (tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && - (range.to === tagList.range.to)) { + (range.to === tagList.range.to)) { return getTagListFromResponse(tagList.results, type, keyValue); } tagList = { - metricName:metricname, - range:range + metricName: metricname, + range: range }; var body = { metrics : [{name : metricname}] }; + convertToKairosTime(range.from, body, 'start'); convertToKairosTime(range.to, body, 'end'); + var options = { url : this.url + '/api/v1/datapoints/query/tags', method : 'POST', data : body }; + return $http(options).then(function(results) { tagList.results = results; return getTagListFromResponse(results, type, keyValue); }); - }; ///////////////////////////////////////////////////////////////////////// @@ -120,13 +129,15 @@ function (angular, _, kbn) { if (!results.data) { return []; } - if (type === "key") { + else if (type === "key") { return _.keys(results.data.queries[0].results[0].tags); } else if (type === "value" && _.has(results.data.queries[0].results[0].tags, keyValue)) { return results.data.queries[0].results[0].tags[keyValue]; } - return []; + else { + return []; + } } /** @@ -151,10 +162,9 @@ function (angular, _, kbn) { var index = 0; _.each(results.data.queries, function(series) { _.each(series.results, function(result) { - - //var target = result.name; var target = plotParams[index].alias; var details = " ( "; + _.each(result.group_by, function(element) { if (element.name === "tag") { _.each(element.group, function(value, key) { @@ -168,10 +178,13 @@ function (angular, _, kbn) { details += 'time_group=' + element.group.group_number + " "; } }); + details += ") "; + if (details !== " ( ) ") { target += details; } + var datapoints = []; for (var i = 0; i < result.values.length; i++) { @@ -184,11 +197,11 @@ function (angular, _, kbn) { } output.push({ target: target, datapoints: datapoints }); }); - index ++; - }); - var output2 = { data: _.flatten(output) }; - return output2; + index++; + }); + + return { data: _.flatten(output) }; } function convertTargetToQuery(options, target) { @@ -201,6 +214,7 @@ function (angular, _, kbn) { }; query.aggregators = []; + if (target.downsampling !== '(NONE)') { query.aggregators.push({ name: target.downsampling, @@ -209,31 +223,37 @@ function (angular, _, kbn) { sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval) }); } + if (target.horizontalAggregators) { _.each(target.horizontalAggregators, function(chosenAggregator) { var returnedAggregator = { name:chosenAggregator.name }; + if (chosenAggregator.sampling_rate) { returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); returnedAggregator.align_sampling = true; returnedAggregator.align_start_time =true; } + if (chosenAggregator.unit) { returnedAggregator.unit = chosenAggregator.unit + 's'; } + if (chosenAggregator.factor && chosenAggregator.name === 'div') { returnedAggregator.divisor = chosenAggregator.factor; } else if (chosenAggregator.factor && chosenAggregator.name === 'scale') { returnedAggregator.factor = chosenAggregator.factor; } + if (chosenAggregator.percentile) { returnedAggregator.percentile = chosenAggregator.percentile; } query.aggregators.push(returnedAggregator); }); } + if (_.isEmpty(query.aggregators)) { delete query.aggregators; } @@ -244,7 +264,9 @@ function (angular, _, kbn) { if (target.groupByTags || target.nonTagGroupBys) { query.group_by = []; - if (target.groupByTags) {query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)});} + if (target.groupByTags) { + query.group_by.push({name: "tag", tags: angular.copy(target.groupByTags)}); + } if (target.nonTagGroupBys) { _.each(target.nonTagGroupBys, function(rawGroupBy) { var formattedGroupBy = angular.copy(rawGroupBy); @@ -276,102 +298,46 @@ function (angular, _, kbn) { var value = matches[1]; var unit = matches[2]; if (value%1 !== 0) { - if (unit === 'ms') {throw new Error('Invalid interval value, cannot be smaller than the millisecond');} + if (unit === 'ms') { + throw new Error('Invalid interval value, cannot be smaller than the millisecond'); + } value = Math.round(kbn.intervals_in_seconds[unit] * value * 1000); unit = 'ms'; - - } - switch (unit) { - case 'ms': - unit = 'milliseconds'; - break; - case 's': - unit = 'seconds'; - break; - case 'm': - unit = 'minutes'; - break; - case 'h': - unit = 'hours'; - break; - case 'd': - unit = 'days'; - break; - case 'w': - unit = 'weeks'; - break; - case 'M': - unit = 'months'; - break; - case 'y': - unit = 'years'; - break; - default: - console.log("Unknown interval ", intervalString); - break; } return { - "value": value, - "unit": unit + value: value, + unit: convertToKairosDBTimeUnit(unit) }; - }; function convertToKairosTime(date, response_obj, start_stop_name) { var name; + if (_.isString(date)) { if (date === 'now') { return; } else if (date.indexOf('now-') >= 0) { - - name = start_stop_name + "_relative"; - date = date.substring(4); + name = start_stop_name + "_relative"; var re_date = /(\d+)\s*(\D+)/; var result = re_date.exec(date); + if (result) { var value = result[1]; var unit = result[2]; - switch (unit) { - case 'ms': - unit = 'milliseconds'; - break; - case 's': - unit = 'seconds'; - break; - case 'm': - unit = 'minutes'; - break; - case 'h': - unit = 'hours'; - break; - case 'd': - unit = 'days'; - break; - case 'w': - unit = 'weeks'; - break; - case 'M': - unit = 'months'; - break; - case 'y': - unit = 'years'; - break; - default: - console.log("Unknown date ", date); - break; - } + response_obj[name] = { - "value": value, - "unit": unit + value: value, + unit: convertToKairosDBTimeUnit(unit) }; return; } console.log("Unparseable date", date); return; } + date = kbn.parseDate(date); } @@ -384,6 +350,30 @@ function (angular, _, kbn) { console.log("Date is neither string nor date"); } + function convertToKairosDBTimeUnit(unit) { + switch (unit) { + case 'ms': + return 'milliseconds'; + case 's': + return 'seconds'; + case 'm': + return 'minutes'; + case 'h': + return 'hours'; + case 'd': + return 'days'; + case 'w': + return 'weeks'; + case 'M': + return 'months'; + case 'y': + return 'years'; + default: + console.log("Unknown unit ", unit); + return ''; + } + } + function PeakFilter(dataIn, limit) { var datapoints = dataIn; var arrLength = datapoints.length; @@ -417,7 +407,6 @@ function (angular, _, kbn) { return datapoints; } - //////////////////////////////////////////////////////////////////////// return KairosDBDatasource; }); diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 9aad7c41fc9..6370afc7c0e 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -37,6 +37,7 @@ function (angular, _) { $scope.get_data(); } }; + $scope.panelBlur = function() { _.each($scope.panel.targets, function(target) { target.downsampling = $scope.panel.downsampling; @@ -49,6 +50,7 @@ function (angular, _) { var clone = angular.copy($scope.target); $scope.panel.targets.push(clone); }; + $scope.moveMetricQuery = function(fromIndex, toIndex) { _.move($scope.panel.targets, fromIndex, toIndex); }; @@ -140,7 +142,6 @@ function (angular, _) { if (!$scope.target.groupByTags) { $scope.target.groupByTags = []; } - console.log($scope.target.groupBy.tagKey); if (!_.contains($scope.target.groupByTags, $scope.target.groupBy.tagKey)) { $scope.target.groupByTags.push($scope.target.groupBy.tagKey); $scope.targetBlur(); @@ -202,12 +203,14 @@ function (angular, _) { errors.tagKey = 'You must supply a tag name'; } } + if ($scope.isValueGroupBy) { if (!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) { errors.valueRange = "Range must be an integer"; $scope.isGroupByValid = false; } } + if ($scope.isTimeGroupBy) { try { $scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval); @@ -265,7 +268,6 @@ function (angular, _) { $scope.hasUnit = false; $scope.hasFactor = false; $scope.hasPercentile = false; - }; $scope.removeHorizontalAggregator = function(index) { @@ -279,7 +281,7 @@ function (angular, _) { $scope.changeHorAggregationInput = function() { $scope.hasSamplingRate = _.contains(['avg','dev','max','min','sum','least_squares','count','percentile'], - $scope.target.currentHorizontalAggregatorName); + $scope.target.currentHorizontalAggregatorName); $scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName); $scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName); $scope.hasPercentile = 'percentile' === $scope.target.currentHorizontalAggregatorName; @@ -290,6 +292,7 @@ function (angular, _) { delete $scope.target.errors.horAggregator; var errors = {}; $scope.isAggregatorValid = true; + if ($scope.hasSamplingRate) { try { $scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate); @@ -298,6 +301,7 @@ function (angular, _) { $scope.isAggregatorValid = false; } } + if ($scope.hasFactor) { if (!$scope.target.horAggregator.factor) { errors.factor = 'You must supply a numeric value for this aggregator'; @@ -308,6 +312,7 @@ function (angular, _) { $scope.isAggregatorValid = false; } } + if ($scope.hasPercentile) { if (!$scope.target.horAggregator.percentile || $scope.target.horAggregator.percentile<=0 || From 869cf705e08b954b9d37c3b69e654b7a642aeaa4 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Wed, 22 Apr 2015 22:29:56 +0300 Subject: [PATCH 028/398] Remove exit 0 at the end of init.d script --- packaging/rpm/init.d/grafana-server | 2 -- 1 file changed, 2 deletions(-) diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index 96e0c18c5e4..a30fffb6e00 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -144,5 +144,3 @@ case "$1" in exit 1 ;; esac - -exit 0 From 2c52224013dbe3b3e9da530d57c0d7175a051b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 23 Apr 2015 08:24:30 +0200 Subject: [PATCH 029/398] /api/login/ping Fix for issue when behind reverse proxy and subpath, Fixes #1857 --- CHANGELOG.md | 5 +++++ package.json | 2 +- public/app/services/backendSrv.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94852d43047..1d656e3685c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.0.3 (unreleased) + +**Fixes** +- [Issue #1857](https://github.com/grafana/grafana/issues/1857). /api/login/ping Fix for issue when behind reverse proxy and subpath + # 2.0.2 (2015-04-22) **Fixes** diff --git a/package.json b/package.json index 0ff6bf8b480..64e393616f6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "2.0.2", + "version": "2.0.3-pre1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" diff --git a/public/app/services/backendSrv.js b/public/app/services/backendSrv.js index 30cad361bbe..c208fe3388e 100644 --- a/public/app/services/backendSrv.js +++ b/public/app/services/backendSrv.js @@ -63,8 +63,9 @@ function (angular, _, config) { var requestIsLocal = options.url.indexOf('/') === 0; var firstAttempt = options.retry === 0; - if (requestIsLocal && firstAttempt) { + if (requestIsLocal && !options.hasSubUrl) { options.url = config.appSubUrl + options.url; + options.hasSubUrl = true; } return $http(options).then(function(results) { From 968b1b430827472057195c798282649982392801 Mon Sep 17 00:00:00 2001 From: aibou Date: Thu, 23 Apr 2015 17:42:04 +0900 Subject: [PATCH 030/398] Fix and update documents --- conf/defaults.ini | 5 +++-- conf/sample.ini | 4 ++-- docs/sources/installation/configuration.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 042f1a5574c..6bb3fb80857 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -7,7 +7,7 @@ app_mode = production #################################### Paths #################################### [paths] -# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD) +# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) # data = data # @@ -62,7 +62,7 @@ path = grafana.db #################################### Session #################################### [session] -# Either "memory", "file", "redis", "mysql", default is "memory" +# Either "memory", "file", "redis", "mysql", "postgresql", default is "file" provider = file # Provider config options @@ -70,6 +70,7 @@ provider = file # file: session dir path, is relative to grafana data_path # redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` + provider_config = sessions # Session cookie name diff --git a/conf/sample.ini b/conf/sample.ini index 6aaa69f7314..68bd3eb3a1d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -7,7 +7,7 @@ #################################### Paths #################################### [paths] -# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD) +# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) # ;data = /var/lib/grafana # @@ -62,7 +62,7 @@ #################################### Session #################################### [session] -# Either "memory", "file", "redis", "mysql", default is "memory" +# Either "memory", "file", "redis", "mysql", "postgresql", default is "file" ;provider = file # Provider config options diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index d727f9ced41..a79a8494d73 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -219,7 +219,7 @@ set to true, any user successfully authenticating via google auth will be automa ## [session] ### provider -Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory". +Valid values are "memory", "file", "mysql", 'postgres'. Default is "file". ### provider_config This option should be configured differently depending on what type of session provider you have configured. From 236c4e65f863d55ae79e4df9a3ff9124d4d4b15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 23 Apr 2015 15:26:39 +0200 Subject: [PATCH 031/398] Began work on dashboard: editable flag, that actually stops users from changing anything, #1834 --- pkg/api/dtos/models.go | 6 +- public/app/features/dashboard/dashboardSrv.js | 20 +-- .../dashboard/partials/dashboardTopNav.html | 12 +- public/app/partials/dasheditor.html | 155 ++++++++++++------ public/app/routes/dashLoadControllers.js | 41 +++-- public/css/less/bootstrap-tagsinput.less | 30 +--- .../vendor/tagsinput/bootstrap-tagsinput.js | 2 +- 7 files changed, 157 insertions(+), 109 deletions(-) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 53519631d22..0057f78ff0d 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -28,9 +28,9 @@ type CurrentUser struct { } type DashboardMeta struct { - IsStarred bool `json:"isStarred"` - IsHome bool `json:"isHome"` - IsSnapshot bool `json:"isSnapshot"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` Slug string `json:"slug"` Expires time.Time `json:"expires"` Created time.Time `json:"created"` diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 1064892745b..73a36ed3da8 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -52,24 +52,20 @@ function (angular, $, kbn, _, moment) { p._initMeta = function(meta) { meta = meta || {}; - meta.canShare = true; - meta.canSave = true; - meta.canEdit = true; - meta.canStar = true; + + meta.canShare = meta.canShare === false ? false : true; + meta.canSave = meta.canSave === false ? false : true; + meta.canEdit = meta.canEdit === false ? false : true; + meta.canStar = meta.canStar === false ? false : true; + meta.canDelete = meta.canDelete === false ? false : true; if (contextSrv.hasRole('Viewer')) { meta.canSave = false; } - if (meta.isSnapshot) { - meta.canSave = false; - } - - if (meta.isHome) { - meta.canShare = false; - meta.canStar = false; - meta.canSave = false; + if (!this.editable) { meta.canEdit = false; + meta.canDelete = false; } this.meta = meta; diff --git a/public/app/features/dashboard/partials/dashboardTopNav.html b/public/app/features/dashboard/partials/dashboardTopNav.html index 784185837e8..711fb1f34ba 100644 --- a/public/app/features/dashboard/partials/dashboardTopNav.html +++ b/public/app/features/dashboard/partials/dashboardTopNav.html @@ -27,19 +27,19 @@
    • -
    • +
    • -
    diff --git a/public/app/partials/dasheditor.html b/public/app/partials/dasheditor.html index e6ab6070f9f..fefded81efc 100644 --- a/public/app/partials/dasheditor.html +++ b/public/app/partials/dasheditor.html @@ -17,63 +17,118 @@
    -
    - -
    -
    -
    -
    - -
    -
    - - -
    - - +
    +
    +
    +
    +
    Dashboard info
    +
    +
      +
    • + Title +
    • +
    • + +
    • +
    • + Tags + Press enter to a add tag +
    • +
    • + + +
    • +
    +
    +
    +
      +
    • + Timezone +
    • +
    • + +
    • +
    +
    +
    +
    -
    -
    -
    - - - - Press enter to a add tag -
    + +
    +
    Toggles
    +
    +
      +
    • + +
    • +
    • +
    • + + +
    • + +
    +
    +
    +
    +
      +
    • + +
    • +
    • + + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + + +
    • +
    +
    - -
    -
    -
    -
    - - - - - - -
    - {{row.title}} - - - - -
    -
    -
    -
    - -
    - - -
    -
    -
    +
    +
    +
    + + + + + + + +
    + {{row.title}} + + + + +
    +
    +
    +
    +
    + +
    + + +
    + +
    + +
- -
+
ADD ROW From d84d92f73c9bdb4578a6ce48540fb38bfab8b710 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Sat, 25 Apr 2015 18:46:30 +0800 Subject: [PATCH 044/398] fixes #1880 correct mysql statement for modifying column data type --- pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 9cbb5c8c7ab..2e3d2d2a188 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -53,5 +53,5 @@ func addDashboardSnapshotMigrations(mg *Migrator) { mg.AddMigration("alter dashboard_snapshot.data to mediumtext v1", new(RawSqlMigration). Sqlite("SELECT 0 WHERE 0;"). Postgres("SELECT 0;"). - Mysql("ALTER TABLE dashboard_snapshot.data MODIFY data MEDIUMTEXT;")) + Mysql("ALTER TABLE dashboard_snapshot MODIFY data MEDIUMTEXT;")) } From f227002a804c4fcdc360939ce5bb7e0b5194a3aa Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Sat, 25 Apr 2015 18:51:25 +0800 Subject: [PATCH 045/398] fixes #1880 dashboard_snapshot table does not have a data column --- pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 2e3d2d2a188..352619ee6e9 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -49,9 +49,4 @@ func addDashboardSnapshotMigrations(mg *Migrator) { mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5)) addTableIndicesMigrations(mg, "v5", snapshotV5) - // ncrease data type - mg.AddMigration("alter dashboard_snapshot.data to mediumtext v1", new(RawSqlMigration). - Sqlite("SELECT 0 WHERE 0;"). - Postgres("SELECT 0;"). - Mysql("ALTER TABLE dashboard_snapshot MODIFY data MEDIUMTEXT;")) } From 5d57931060419416203eb55a049dd13478c1687a Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sat, 25 Apr 2015 23:48:47 +0900 Subject: [PATCH 046/398] Fix appearance of query editor --- .../kairosdb/partials/query.editor.html | 102 +++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html index 052bea2d078..6fa3401dad7 100644 --- a/public/app/plugins/datasource/kairosdb/partials/query.editor.html +++ b/public/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -7,40 +7,21 @@
+
+
+
- + +
+
- + +
+ From e410fbb558be6ae491523a413872b24002028211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 25 Apr 2015 20:28:50 +0200 Subject: [PATCH 047/398] Corrected SQL migration for snapshot table column type change, #1880 --- pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 352619ee6e9..b08cc451e55 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -49,4 +49,9 @@ func addDashboardSnapshotMigrations(mg *Migrator) { mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5)) addTableIndicesMigrations(mg, "v5", snapshotV5) + // change column type of dashboard + mg.AddMigration("alter dashboard_snapshot to mediumtext v2", new(RawSqlMigration). + Sqlite("SELECT 0 WHERE 0;"). + Postgres("SELECT 0;"). + Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;")) } From 7e044e29baebcce3747bb17ee0b96ed2ddeb9f1e Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 26 Apr 2015 11:37:17 +0900 Subject: [PATCH 048/398] Change metric form input text Preparation of templated dashboard support. --- .../plugins/datasource/kairosdb/datasource.js | 7 ++-- .../kairosdb/partials/query.editor.html | 17 +++++----- .../plugins/datasource/kairosdb/queryCtrl.js | 32 ++++++------------- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index 9d25b694891..64e9c4c9b49 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -18,7 +18,6 @@ function (angular, _, kbn) { this.url = datasource.url; this.name = datasource.name; this.supportMetrics = true; - this.grafanaDB = datasource.grafanaDB; } // Called once per panel (graph) @@ -85,11 +84,11 @@ function (angular, _, kbn) { method : 'GET' }; - return $http(options).then(function(results) { - if (!results.data) { + return $http(options).then(function(response) { + if (!response.data) { return []; } - return results.data.results; + return response.data.results; }); }; diff --git a/public/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html index dc272ebbf4c..eacc78f8819 100644 --- a/public/app/plugins/datasource/kairosdb/partials/query.editor.html +++ b/public/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -61,14 +61,15 @@ spellcheck='false' placeholder="alias" ng-blur="targetBlur()">
  • - + diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 6370afc7c0e..d7ec94f5205 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -6,16 +6,12 @@ function (angular, _) { 'use strict'; var module = angular.module('grafana.controllers'); - var metricList = null; + var metricList = []; var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; module.controller('KairosDBQueryCtrl', function($scope) { $scope.init = function() { - $scope.metric = { - list: ["Loading..."], - value: "Loading..." - }; $scope.panel.stack = false; if (!$scope.panel.downsampling) { $scope.panel.downsampling = 'avg'; @@ -25,12 +21,10 @@ function (angular, _) { $scope.target.sampling = $scope.panel.sampling; } $scope.targetLetters = targetLetters; - $scope.updateMetricList(); $scope.target.errors = validateTarget($scope.target); }; $scope.targetBlur = function() { - $scope.target.metric = $scope.metric.value; $scope.target.errors = validateTarget($scope.target); if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { $scope.oldTarget = angular.copy($scope.target); @@ -55,22 +49,16 @@ function (angular, _) { _.move($scope.panel.targets, fromIndex, toIndex); }; - // Fetch metric list - $scope.updateMetricList = function() { - $scope.metricListLoading = true; - metricList = []; - $scope.datasource.performMetricSuggestQuery().then(function(series) { - metricList = series; - $scope.metric.list = series; - if ($scope.target.metric) { - $scope.metric.value = $scope.target.metric; - } - else { - $scope.metric.value = ""; - } - $scope.metricListLoading = false; + $scope.suggestMetrics = function(query, callback) { + if (!_.isEmpty(metricList)) { return metricList; - }); + } + else { + $scope.datasource.performMetricSuggestQuery().then(function(result) { + metricList = result; + callback(metricList); + }); + } }; $scope.suggestTagKeys = function(query, callback) { From d86814b6c5af1ff6c30a7a20c42c4f0eb2f1fe8a Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 26 Apr 2015 18:18:43 +0900 Subject: [PATCH 049/398] Refactoring of tag suggestion --- .../plugins/datasource/kairosdb/datasource.js | 49 +++++-------------- .../plugins/datasource/kairosdb/queryCtrl.js | 32 ++++++++++-- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js index 64e9c4c9b49..b97a06fea04 100644 --- a/public/app/plugins/datasource/kairosdb/datasource.js +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -8,7 +8,6 @@ function (angular, _, kbn) { 'use strict'; var module = angular.module('grafana.services'); - var tagList = null; module.factory('KairosDBDatasource', function($q, $http) { @@ -92,31 +91,24 @@ function (angular, _, kbn) { }); }; - KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname, range, type, keyValue) { - if (tagList && (metricname === tagList.metricName) && (range.from === tagList.range.from) && - (range.to === tagList.range.to)) { - return getTagListFromResponse(tagList.results, type, keyValue); - } - tagList = { - metricName: metricname, - range: range - }; - var body = { - metrics : [{name : metricname}] - }; - - convertToKairosTime(range.from, body, 'start'); - convertToKairosTime(range.to, body, 'end'); - + KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname) { var options = { url : this.url + '/api/v1/datapoints/query/tags', method : 'POST', - data : body + data : { + metrics : [{ name : metricname }], + cache_time : 0, + start_absolute: 0 + } }; - return $http(options).then(function(results) { - tagList.results = results; - return getTagListFromResponse(results, type, keyValue); + return $http(options).then(function(response) { + if (!response.data) { + return []; + } + else { + return response.data.queries[0].results[0]; + } }); }; @@ -124,21 +116,6 @@ function (angular, _, kbn) { /// Formatting methods //////////////////////////////////////////////////////////////////////// - function getTagListFromResponse(results, type, keyValue) { - if (!results.data) { - return []; - } - else if (type === "key") { - return _.keys(results.data.queries[0].results[0].tags); - } - else if (type === "value" && _.has(results.data.queries[0].results[0].tags, keyValue)) { - return results.data.queries[0].results[0].tags[keyValue]; - } - else { - return []; - } - } - /** * Requires a verion of KairosDB with every CORS defects fixed * @param results diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index 716274ffe0a..c2fb5055ea5 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -7,6 +7,7 @@ function (angular, _) { var module = angular.module('grafana.controllers'); var metricList = []; + var tagList = []; var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; module.controller('KairosDBQueryCtrl', function($scope) { @@ -63,12 +64,37 @@ function (angular, _) { }; $scope.suggestTagKeys = function(query, callback) { - callback($scope.datasource.performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'key', '')); + if (!_.isEmpty(tagList)) { + var result = _.find(tagList, { name : $scope.target.metric }); + + if (!_.isEmpty(result)) { + return _.keys(result.tags); + } + } + + $scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) { + if (!_.isEmpty(result)) { + tagList.push(result); + callback(_.keys(result.tags)); + } + }); }; $scope.suggestTagValues = function(query, callback) { - callback($scope.datasource - .performTagSuggestQuery($scope.target.metric, $scope.rangeUnparsed, 'value', $scope.target.currentTagKey)); + if (!_.isEmpty(tagList)) { + var result = _.find(tagList, { name : $scope.target.metric }); + + if (!_.isEmpty(result)) { + return result.tags[$scope.target.currentTagKey]; + } + } + + $scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) { + if (!_.isEmpty(result)) { + tagList.push(result); + callback(result.tags[$scope.target.currentTagKey]); + } + }); }; // Filter metric by tag From df9403809b42499cb896677f02e2a9f5953d9dfb Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Sun, 26 Apr 2015 23:14:50 +0900 Subject: [PATCH 050/398] Fix conflict --- public/app/plugins/datasource/kairosdb/queryCtrl.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js index c2fb5055ea5..9e8c5817dd1 100644 --- a/public/app/plugins/datasource/kairosdb/queryCtrl.js +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -8,7 +8,6 @@ function (angular, _) { var module = angular.module('grafana.controllers'); var metricList = []; var tagList = []; - var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; module.controller('KairosDBQueryCtrl', function($scope) { @@ -21,8 +20,6 @@ function (angular, _) { $scope.target.downsampling = $scope.panel.downsampling; $scope.target.sampling = $scope.panel.sampling; } - $scope.targetLetters = targetLetters; - $scope.updateMetricList(); $scope.target.errors = validateTarget($scope.target); }; From 16fc6e8b4234dbffda88af6091a47f70f3a6bc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 10:09:32 +0200 Subject: [PATCH 051/398] More work on editable false dashboards, #1834 --- .../features/dashboard/dashboardNavCtrl.js | 2 ++ public/app/features/dashboard/dashboardSrv.js | 2 ++ .../dashboard/partials/dashboardTopNav.html | 4 ++-- public/app/routes/dashLoadControllers.js | 17 ++++++++++---- public/app/services/contextSrv.js | 15 ++++++------- public/test/specs/dashboardSrv-specs.js | 22 ++++++++++++++++--- 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dashboardNavCtrl.js b/public/app/features/dashboard/dashboardNavCtrl.js index 11595e30479..d2cf8d29c12 100644 --- a/public/app/features/dashboard/dashboardNavCtrl.js +++ b/public/app/features/dashboard/dashboardNavCtrl.js @@ -119,6 +119,8 @@ function (angular, _) { $scope.saveDashboardAs = function() { var newScope = $rootScope.$new(); newScope.clone = $scope.dashboard.getSaveModelClone(); + newScope.clone.editable = true; + newScope.clone.hideControls = false; $scope.appEvent('show-modal', { src: './app/features/dashboard/partials/saveDashboardAs.html', diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 73a36ed3da8..9324f18e33b 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -66,6 +66,8 @@ function (angular, $, kbn, _, moment) { if (!this.editable) { meta.canEdit = false; meta.canDelete = false; + meta.canSave = false; + this.hideControls = true; } this.meta = meta; diff --git a/public/app/features/dashboard/partials/dashboardTopNav.html b/public/app/features/dashboard/partials/dashboardTopNav.html index 711fb1f34ba..6f85d6b4e02 100644 --- a/public/app/features/dashboard/partials/dashboardTopNav.html +++ b/public/app/features/dashboard/partials/dashboardTopNav.html @@ -27,7 +27,7 @@
  • -
  • +
  • Templating
  • Export
  • View JSON
  • -
  • Save As...
  • +
  • Save As...
  • Delete dashboard
  • diff --git a/public/app/routes/dashLoadControllers.js b/public/app/routes/dashLoadControllers.js index ba33dc7e73a..ee36ef72117 100644 --- a/public/app/routes/dashLoadControllers.js +++ b/public/app/routes/dashLoadControllers.js @@ -59,12 +59,15 @@ function (angular, _, kbn, moment, $) { $location.path(''); return; } - $scope.initDashboard({meta: {}, model: window.grafanaImportDashboard }, $scope); + $scope.initDashboard({ + meta: { canShare: false, canStar: false }, + model: window.grafanaImportDashboard + }, $scope); }); module.controller('NewDashboardCtrl', function($scope) { $scope.initDashboard({ - meta: {}, + meta: { canStar: false, canShare: false }, model: { title: "New dashboard", rows: [{ height: '250px', panels:[] }] @@ -93,7 +96,10 @@ function (angular, _, kbn, moment, $) { }; file_load($routeParams.jsonFile).then(function(result) { - $scope.initDashboard({meta: {fromFile: true}, model: result}, $scope); + $scope.initDashboard({ + meta: { canSave: false, canDelete: false }, + model: result + }, $scope); }); }); @@ -138,7 +144,10 @@ function (angular, _, kbn, moment, $) { }; script_load($routeParams.jsFile).then(function(result) { - $scope.initDashboard({meta: {fromScript: true, canDelete: false}, model: result.data}, $scope); + $scope.initDashboard({ + meta: {fromScript: true, canDelete: false, canSave: false}, + model: result.data + }, $scope); }); }); diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index c615e0baf8c..99bbcccf156 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -18,13 +18,6 @@ function (angular, _, store, config) { } } - this.version = config.buildInfo.version; - this.lightTheme = false; - this.user = new User(); - this.isSignedIn = this.user.isSignedIn; - this.isGrafanaAdmin = this.user.isGrafanaAdmin; - this.sidemenu = store.getBool('grafana.sidemenu'); - // events $rootScope.$on('toggle-sidemenu', function() { self.toggleSideMenu(); @@ -47,6 +40,12 @@ function (angular, _, store, config) { }, 50); }; + this.version = config.buildInfo.version; + this.lightTheme = false; + this.user = new User(); + this.isSignedIn = this.user.isSignedIn; + this.isGrafanaAdmin = this.user.isGrafanaAdmin; + this.sidemenu = store.getBool('grafana.sidemenu'); + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); }); - }); diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index 19f81f1cc01..35d248888c7 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -185,10 +185,26 @@ define([ expect(model.annotations.list.length).to.be(0); expect(model.templating.list.length).to.be(0); }); - }); + describe('Given editable false dashboard', function() { + var model; + + beforeEach(function() { + model = _dashboardSrv.create({ + editable: false, + }); + }); + + it('Should set meta canEdit and canSave to false', function() { + expect(model.meta.canSave).to.be(false); + expect(model.meta.canEdit).to.be(false); + }); + + it('getSaveModelClone should remove meta', function() { + var clone = model.getSaveModelClone(); + expect(clone.meta).to.be(undefined); + }); + }); }); - - }); From 29a7490af294887ec03db1aff2fd5d78ded49ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 10:29:04 +0200 Subject: [PATCH 052/398] Small fix for unsaved changes when using save as feature --- public/app/features/dashboard/unsavedChangesSrv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index d17276dc374..eec884551ec 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -37,7 +37,7 @@ function(angular, _, config) { }); this.ignoreChanges = function() { - if (!self.current) { return true; } + if (!self.current || !self.current.meta) { return true; } var meta = self.current.meta; return !meta.canSave || meta.fromScript || meta.fromFile; From b0ef659add252ae47dea6a5730ffca2a9051401c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 10:56:36 +0200 Subject: [PATCH 053/398] Stop users from entering panel edit mode when dashboard editable is false, #1834 --- public/app/features/panel/panelSrv.js | 8 ++++++++ public/app/panels/graph/module.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index d8e2272436a..33f70ca25da 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -70,6 +70,14 @@ function (angular, _, config) { }; $scope.toggleFullscreen = function(edit) { + if (edit && $scope.dashboardMeta.canEdit === false) { + $scope.appEvent('alert-warning', [ + 'Dashboard not editable', + 'Use Save As.. feature to create an editable copy of this dashboard.' + ]); + return; + } + $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id }); }; diff --git a/public/app/panels/graph/module.js b/public/app/panels/graph/module.js index b9fff58f670..dfcd5901594 100644 --- a/public/app/panels/graph/module.js +++ b/public/app/panels/graph/module.js @@ -29,7 +29,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { panelName: 'Graph', editIcon: "fa fa-bar-chart", fullscreen: true, - metricsEditor: true + metricsEditor: true, }); $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html'); From 8a986ec340948ba3ce0b9954ffba29e112822b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 10:59:14 +0200 Subject: [PATCH 054/398] Using CTRL+S should not work when dashboardMeta.canSave is false, #1834 --- public/app/features/dashboard/dashboardNavCtrl.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/dashboard/dashboardNavCtrl.js b/public/app/features/dashboard/dashboardNavCtrl.js index d2cf8d29c12..27ce528f69e 100644 --- a/public/app/features/dashboard/dashboardNavCtrl.js +++ b/public/app/features/dashboard/dashboardNavCtrl.js @@ -52,6 +52,10 @@ function (angular, _) { }; $scope.saveDashboard = function(options) { + if ($scope.dashboardMeta.canSave === false) { + return; + } + var clone = $scope.dashboard.getSaveModelClone(); backendSrv.saveDashboard(clone, options).then(function(data) { 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 055/398] 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 056/398] 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 5cb7721ab21cb93637202a3bd4cfc0bdbf900eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 15:06:42 +0200 Subject: [PATCH 057/398] Updated config sample.ini with oauth allow_sign_up --- conf/defaults.ini | 3 ++- conf/sample.ini | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 6bb3fb80857..e6e82badb24 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -137,6 +137,7 @@ org_role = Viewer #################################### Github Auth ########################## [auth.github] enabled = false +allow_sign_up = false client_id = some_id client_secret = some_secret scopes = user:email @@ -144,11 +145,11 @@ auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user allowed_domains = -allow_sign_up = false #################################### Google Auth ########################## [auth.google] enabled = false +allow_sign_up = false client_id = some_client_id client_secret = some_client_secret scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email diff --git a/conf/sample.ini b/conf/sample.ini index 68bd3eb3a1d..c0b427fd1a4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -136,26 +136,26 @@ #################################### Github Auth ########################## [auth.github] ;enabled = false +;allow_sign_up = false ;client_id = some_id ;client_secret = some_secret ;scopes = user:email ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user -# Uncomment bellow to only allow specific email domains -; allowed_domains = mycompany.com othercompany.com +;allowed_domains = #################################### Google Auth ########################## [auth.google] ;enabled = false +;allow_sign_up = false ;client_id = some_client_id ;client_secret = some_client_secret ;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email ;auth_url = https://accounts.google.com/o/oauth2/auth ;token_url = https://accounts.google.com/o/oauth2/token ;api_url = https://www.googleapis.com/oauth2/v1/userinfo -# Uncomment bellow to only allow specific email domains -; allowed_domains = mycompany.com othercompany.com +;allowed_domains = #################################### Logging ########################## [log] From 5175cf70ef53a551c9b51d1981b8577859eb6289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 27 Apr 2015 15:07:13 +0200 Subject: [PATCH 058/398] fixed version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8785632eaee..64e393616f6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "2.1.0-pre1", + "version": "2.0.3-pre1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" 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 059/398] 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 061/398] 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 062/398] 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 063/398] 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 064/398] 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 065/398] 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 066/398] 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 067/398] 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 068/398] 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 069/398] 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 070/398] 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 c4ac3d61b167a2286d133267023e0890d38a66d4 Mon Sep 17 00:00:00 2001 From: davidak Date: Tue, 28 Apr 2015 20:53:29 +0200 Subject: [PATCH 071/398] add gigabytes as unit --- public/app/components/kbn.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index fa2b8214873..652b7d6c923 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -376,6 +376,7 @@ function($, _, moment) { kbn.valueFormats.bytes = kbn.formatFuncCreator(1024, [' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.kbytes = kbn.formatFuncCreator(1024, [' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.mbytes = kbn.formatFuncCreator(1024, [' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); + kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']); @@ -547,6 +548,7 @@ function($, _, moment) { {text: 'bytes', value: 'bytes'}, {text: 'kilobytes', value: 'kbytes'}, {text: 'megabytes', value: 'mbytes'}, + {text: 'gigabytes', value: 'gbytes'}, ] }, { From e5844afb0f394876b5396923a59d56fd96695e65 Mon Sep 17 00:00:00 2001 From: David Raifaizen Date: Tue, 28 Apr 2015 16:29:32 -0400 Subject: [PATCH 072/398] Corrected missing annotations: true tag from the influxdb9 plugin json and fixed influxseries data handling --- .../datasource/influxdb/influxSeries.js | 21 +++++++------------ .../plugins/datasource/influxdb/plugin.json | 3 ++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index 9e28884547b..cca01459fc4 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -5,7 +5,8 @@ function (_) { 'use strict'; function InfluxSeries(options) { - this.seriesList = options.seriesList; + this.seriesList = options.seriesList && options.seriesList.results && options.seriesList.results.length > 0 + ? options.seriesList.results[0].series || [] : []; this.alias = options.alias; this.annotation = options.annotation; } @@ -17,12 +18,10 @@ function (_) { var self = this; console.log(self.seriesList); - if (!self.seriesList || !self.seriesList.results || !self.seriesList.results[0]) { + if (self.seriesList.length === 0) { return output; } - this.seriesList = self.seriesList.results[0].series; - _.each(self.seriesList, function(series) { var datapoints = []; for (var i = 0; i < series.values.length; i++) { @@ -63,19 +62,15 @@ function (_) { if (column === self.annotation.textColumn) { textCol = index; return; } }); - _.each(series.points, function (point) { + _.each(series.values, function (value) { var data = { annotation: self.annotation, - time: point[timeCol], - title: point[titleCol], - tags: point[tagsCol], - text: point[textCol] + time: + new Date(value[timeCol]), + title: value[titleCol], + tags: value[tagsCol], + text: value[textCol] }; - if (tagsCol) { - data.tags = point[tagsCol]; - } - list.push(data); }); }); diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index 40c5fc9cea9..c93b41747a7 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -13,5 +13,6 @@ "annotations": "app/plugins/datasource/influxdb/partials/annotations.editor.html" }, - "metrics": true + "metrics": true, + "annotations": true } From a0dad3897479533eb8b8b181404876306f4e278a Mon Sep 17 00:00:00 2001 From: Ian Danforth Date: Tue, 28 Apr 2015 15:09:22 -0700 Subject: [PATCH 073/398] Correct deb version number in install guide. --- docs/sources/installation/debian.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4bba51ec530..c2594e38445 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -16,7 +16,7 @@ Description | Download $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_2.0.1_amd64.deb + $ sudo dpkg -i grafana_2.0.2_amd64.deb ## APT Repository Add the following line to your `/etc/apt/sources.list` From 7ea579bb71cf1d505c815d5907b4d8910772a488 Mon Sep 17 00:00:00 2001 From: Garrett Bjerkhoel Date: Tue, 28 Apr 2015 20:19:48 -0700 Subject: [PATCH 074/398] Add team_ids configuration option --- conf/defaults.ini | 1 + pkg/social/social.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 6bb3fb80857..7345f18520e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -140,6 +140,7 @@ enabled = false client_id = some_id client_secret = some_secret scopes = user:email +team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user diff --git a/pkg/social/social.go b/pkg/social/social.go index 47c7ea5dc38..c19784e34e1 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -75,7 +75,8 @@ func NewOAuthService() { // GitHub. if name == "github" { setting.OAuthService.GitHub = true - SocialMap["github"] = &SocialGithub{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup} + teamIds := sec.Key("team_ids").Ints(",") + SocialMap["github"] = &SocialGithub{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup, teamIds: teamIds} } // Google. @@ -105,6 +106,7 @@ type SocialGithub struct { allowedDomains []string ApiUrl string allowSignup bool + teamIds []int } func (s *SocialGithub) Type() int { From 979d0ca70f5aec638ce6e6bb8c8f69f47973f221 Mon Sep 17 00:00:00 2001 From: Garrett Bjerkhoel Date: Tue, 28 Apr 2015 20:21:44 -0700 Subject: [PATCH 075/398] Add new error type for team membership permissions --- pkg/social/social.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/social/social.go b/pkg/social/social.go index c19784e34e1..752ed7114c2 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "errors" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -109,6 +110,10 @@ type SocialGithub struct { teamIds []int } +var ( + ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") +) + func (s *SocialGithub) Type() int { return int(models.GITHUB) } From eb37fc089b01aba3d046635e60596e8d82dc1601 Mon Sep 17 00:00:00 2001 From: Garrett Bjerkhoel Date: Tue, 28 Apr 2015 20:22:21 -0700 Subject: [PATCH 076/398] Check for active team membership when fetching s.UserInfo --- pkg/social/social.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/pkg/social/social.go b/pkg/social/social.go index 752ed7114c2..2b716495d53 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" "errors" + "net/http" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -126,6 +127,28 @@ func (s *SocialGithub) IsSignupAllowed() bool { return s.allowSignup } +func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool { + var data struct { + Url string `json:"url"` + State string `json:"state"` + } + + membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username) + r, err := client.Get(membershipUrl) + if err != nil { + return false + } + + defer r.Body.Close() + + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return false + } + + active := data.State == "active" + return active +} + func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { var data struct { Id int `json:"id"` @@ -146,11 +169,23 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, err } - return &BasicUserInfo{ + userInfo := &BasicUserInfo{ Identity: strconv.Itoa(data.Id), Name: data.Name, Email: data.Email, - }, nil + } + + if len(s.teamIds) > 0 { + for _, teamId := range s.teamIds { + if s.IsTeamMember(client, data.Name, teamId) { + return userInfo, nil + } + } + + return nil, ErrMissingTeamMembership + } else { + return userInfo, nil + } } // ________ .__ From 1d7f9452685f5f2d41224afbf659bb922636a8bc Mon Sep 17 00:00:00 2001 From: Garrett Bjerkhoel Date: Tue, 28 Apr 2015 20:22:45 -0700 Subject: [PATCH 077/398] Handle special error case if connect.UserInfo returns an error --- pkg/api/login_oauth.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 11d62754a18..d89a7237d35 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -45,7 +45,11 @@ func OAuthLogin(ctx *middleware.Context) { userInfo, err := connect.UserInfo(token) if err != nil { - ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) + if err == social.ErrMissingTeamMembership { + ctx.Redirect(setting.AppSubUrl + "/login?missing_team_membership=1") + } else { + ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) + } return } From 1fdc5277ae875b50ee680dcc7f162ef716cdc9c4 Mon Sep 17 00:00:00 2001 From: Garrett Bjerkhoel Date: Tue, 28 Apr 2015 20:38:05 -0700 Subject: [PATCH 078/398] Update documentation for team_ids option --- docs/sources/installation/configuration.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index a79a8494d73..a1dfb6ac035 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -179,6 +179,7 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example client_id = YOUR_GITHUB_APP_CLIENT_ID client_secret = YOUR_GITHUB_APP_CLIENT_SECRET scopes = user:email + team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token allow_sign_up = false @@ -189,6 +190,21 @@ now login or signup with your github accounts. You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is set to true, any user successfully authenticating via github auth will be automatically signed up. +### team_ids +Require an active team membership for at least one of the given teams on GitHub. +If the authenticated user isn't a member of at least one the teams they will not +be able to register or authenticate with your Grafana instance. Example: + + [auth.github] + enabled = true + client_id = YOUR_GITHUB_APP_CLIENT_ID + client_secret = YOUR_GITHUB_APP_CLIENT_SECRET + scopes = user:email + team_ids = 150,300 + auth_url = https://github.com/login/oauth/authorize + token_url = https://github.com/login/oauth/access_token + allow_sign_up = false + ## [auth.google] You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project). When you create the project you will need to specify a callback URL. Specify this as callback: @@ -257,5 +273,3 @@ enabled. Counters are sent every 24 hours. Default value is `true`. ### google_analytics_ua_id If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID here. By defualt this feature is disabled. - - From b72eba1ef223e2476ab4a04663ad74da1b58d1a6 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Wed, 29 Apr 2015 14:55:39 +0800 Subject: [PATCH 079/398] refactor handling of refresh when urlValues being used for templates. fixes #1862 --- .../features/templating/templateValuesSrv.js | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 3df2c831b6f..10ff556bf84 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -28,17 +28,11 @@ function (angular, _, kbn) { for (var i = 0; i < this.variables.length; i++) { var variable = this.variables[i]; var urlValue = queryParams['var-' + variable.name]; - if (variable.refresh) { - promises.push(this.updateOptions(variable)); - } if (urlValue !== void 0) { - var option = _.findWhere(variable.options, { text: urlValue }); - option = option || { text: urlValue, value: urlValue }; - - var promise = this.setVariableValue(variable, option, true); - this.updateAutoInterval(variable); - - promises.push(promise); + promises.push(this.setVariableFromUrl(variable, urlValue)); + } + else if (variable.refresh) { + promises.push(this.updateOptions(variable)); } else if (variable.type === 'interval') { this.updateAutoInterval(variable); @@ -48,6 +42,25 @@ function (angular, _, kbn) { return $q.all(promises); }; + this.setVariableFromUrl = function(variable, urlValue) { + if (variable.refresh) { + var self = this; + //refresh the list of options before setting the value + return this.updateOptions(variable).then(function() { + var option = _.findWhere(variable.options, { text: urlValue }); + option = option || { text: urlValue, value: urlValue }; + + self.updateAutoInterval(variable); + return self.setVariableValue(variable, option); + }); + } + var option = _.findWhere(variable.options, { text: urlValue }); + option = option || { text: urlValue, value: urlValue }; + + this.updateAutoInterval(variable); + return this.setVariableValue(variable, option); + }; + this.updateAutoInterval = function(variable) { if (!variable.auto) { return; } From 81636d8634fa21f60a867ca4a11a270df883d473 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Wed, 29 Apr 2015 15:14:49 +0800 Subject: [PATCH 080/398] expose orgId via currentUser object. fixes #1907 --- pkg/api/dtos/models.go | 1 + pkg/api/index.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 0057f78ff0d..a14ff55c079 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -23,6 +23,7 @@ type CurrentUser struct { LightTheme bool `json:"lightTheme"` OrgRole m.RoleType `json:"orgRole"` OrgName string `json:"orgName"` + OrgId int64 `json:"orgId"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` } diff --git a/pkg/api/index.go b/pkg/api/index.go index d9ecf65b699..6e880b1d066 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -20,6 +20,7 @@ func setIndexViewData(c *middleware.Context) error { LightTheme: c.Theme == "light", OrgName: c.OrgName, OrgRole: c.OrgRole, + OrgId: c.OrgId, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, } From e7c43bf614ae380c99f9cf09b4bdbb100088efad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 09:26:12 +0200 Subject: [PATCH 081/398] expose org id to frontend, Closes #1907 --- pkg/api/dtos/models.go | 3 ++- pkg/api/index.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 0057f78ff0d..fea88f07550 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -21,8 +21,9 @@ type CurrentUser struct { Email string `json:"email"` Name string `json:"name"` LightTheme bool `json:"lightTheme"` - OrgRole m.RoleType `json:"orgRole"` + OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` + OrgRole m.RoleType `json:"orgRole"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` } diff --git a/pkg/api/index.go b/pkg/api/index.go index d9ecf65b699..86a5e3f1882 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -18,6 +18,7 @@ func setIndexViewData(c *middleware.Context) error { Email: c.Email, Name: c.Name, LightTheme: c.Theme == "light", + OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), From 884dc53f8a7bf963e802eb551fc845749516d6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 09:49:22 +0200 Subject: [PATCH 082/398] =?UTF-8?q?smal=C4=BA=20refactorings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/social/social.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/social/social.go b/pkg/social/social.go index 2b716495d53..355f85b54b6 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -2,11 +2,11 @@ package social import ( "encoding/json" + "errors" "fmt" + "net/http" "strconv" "strings" - "errors" - "net/http" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -78,13 +78,23 @@ func NewOAuthService() { if name == "github" { setting.OAuthService.GitHub = true teamIds := sec.Key("team_ids").Ints(",") - SocialMap["github"] = &SocialGithub{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup, teamIds: teamIds} + SocialMap["github"] = &SocialGithub{ + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + teamIds: teamIds, + } } // Google. if name == "google" { setting.OAuthService.Google = true - SocialMap["google"] = &SocialGoogle{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup} + SocialMap["google"] = &SocialGoogle{ + Config: &config, allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + } } } } @@ -106,7 +116,7 @@ func isEmailAllowed(email string, allowedDomains []string) bool { type SocialGithub struct { *oauth2.Config allowedDomains []string - ApiUrl string + apiUrl string allowSignup bool teamIds []int } @@ -129,8 +139,8 @@ func (s *SocialGithub) IsSignupAllowed() bool { func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool { var data struct { - Url string `json:"url"` - State string `json:"state"` + Url string `json:"url"` + State string `json:"state"` } membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username) @@ -158,7 +168,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { var err error client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.ApiUrl) + r, err := client.Get(s.apiUrl) if err != nil { return nil, err } @@ -198,7 +208,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { type SocialGoogle struct { *oauth2.Config allowedDomains []string - ApiUrl string + apiUrl string allowSignup bool } @@ -223,7 +233,7 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { var err error client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.ApiUrl) + r, err := client.Get(s.apiUrl) if err != nil { return nil, err } From 32fa8180fa2bc3c57192a6dce93e34cc4761ad35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 09:52:52 +0200 Subject: [PATCH 083/398] Github OAuth: You can now configure a Github team membership requirement, Closes #1731 --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9133bf8601..70aaa67ab22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -# 2.0.3 (unreleased) +# 2.1.0 (unreleased - master branch) + +**Backend** +- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski + + +# 2.0.3 (unreleased - 2.0.x branch) **Fixes** - [Issue #1872](https://github.com/grafana/grafana/issues/1872). Firefox/IE issue, invisible text in dashboard search fixed From 0d3fbb865929becce7ff5cc72ab0a15d4f1b7da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:08:01 +0200 Subject: [PATCH 084/398] Added message alerts when login failed due to github team membership or email domain requirement, #1731, #1660 --- conf/sample.ini | 7 +++---- pkg/api/login_oauth.go | 5 +++-- public/app/controllers/loginCtrl.js | 9 ++++++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/conf/sample.ini b/conf/sample.ini index 68bd3eb3a1d..4ebab72d1af 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -142,8 +142,8 @@ ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user -# Uncomment bellow to only allow specific email domains -; allowed_domains = mycompany.com othercompany.com +;team_ids = +;allowed_domains = #################################### Google Auth ########################## [auth.google] @@ -154,8 +154,7 @@ ;auth_url = https://accounts.google.com/o/oauth2/auth ;token_url = https://accounts.google.com/o/oauth2/token ;api_url = https://www.googleapis.com/oauth2/v1/userinfo -# Uncomment bellow to only allow specific email domains -; allowed_domains = mycompany.com othercompany.com +;allowed_domains = #################################### Logging ########################## [log] diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index d89a7237d35..505c17ddde8 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "net/url" "golang.org/x/oauth2" @@ -46,7 +47,7 @@ func OAuthLogin(ctx *middleware.Context) { userInfo, err := connect.UserInfo(token) if err != nil { if err == social.ErrMissingTeamMembership { - ctx.Redirect(setting.AppSubUrl + "/login?missing_team_membership=1") + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled")) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } @@ -58,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) { // validate that the email is allowed to login to grafana if !connect.IsEmailAllowed(userInfo.Email) { log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email) - ctx.Redirect(setting.AppSubUrl + "/login?email_not_allowed=1") + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled")) return } diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index 5de773842f8..c8856df0690 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -7,7 +7,7 @@ function (angular, config) { var module = angular.module('grafana.controllers'); - module.controller('LoginCtrl', function($scope, backendSrv, contextSrv) { + module.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { $scope.formModel = { user: '', email: '', @@ -28,6 +28,13 @@ function (angular, config) { $scope.init = function() { $scope.$watch("loginMode", $scope.loginModeChanged); $scope.passwordChanged(); + + var params = $location.search(); + if (params.failedMsg) { + $scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]); + delete params.failedMsg; + $location.search(params); + } }; // build info view model From 3007add4ca4ddb865fb20216c49aa14c00066ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:10:44 +0200 Subject: [PATCH 085/398] small docs fix --- docs/sources/installation/debian.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index cf5e4f4f072..aed4eb420b5 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -16,7 +16,7 @@ Description | Download $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.1_amd64.deb $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_2.0.1_amd64.deb + $ sudo dpkg -i grafana_2.0.2_amd64.deb ## APT Repository Add the following line to your `/etc/apt/sources.list` From a0e80e5869aee5835151b3156a36e0a1a6498cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:13:00 +0200 Subject: [PATCH 086/398] Updated 2.0 docs, merged with master and removed docs for features in 2.1, doc updates related to 2.0 needs to be done to this branch --- docs/sources/installation/configuration.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index a1dfb6ac035..f55561e6668 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -179,7 +179,6 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example client_id = YOUR_GITHUB_APP_CLIENT_ID client_secret = YOUR_GITHUB_APP_CLIENT_SECRET scopes = user:email - team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token allow_sign_up = false @@ -190,21 +189,6 @@ now login or signup with your github accounts. You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is set to true, any user successfully authenticating via github auth will be automatically signed up. -### team_ids -Require an active team membership for at least one of the given teams on GitHub. -If the authenticated user isn't a member of at least one the teams they will not -be able to register or authenticate with your Grafana instance. Example: - - [auth.github] - enabled = true - client_id = YOUR_GITHUB_APP_CLIENT_ID - client_secret = YOUR_GITHUB_APP_CLIENT_SECRET - scopes = user:email - team_ids = 150,300 - auth_url = https://github.com/login/oauth/authorize - token_url = https://github.com/login/oauth/access_token - allow_sign_up = false - ## [auth.google] You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project). When you create the project you will need to specify a callback URL. Specify this as callback: From 4c7545e909df74299b91b29be556e4acd888398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:13:52 +0200 Subject: [PATCH 087/398] Updated docs Makefile --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index d44bc545e2c..fcb1708f916 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,7 +44,7 @@ docs-test: docs-build $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh docs-build: - git fetch https://github.com/grafana/grafana.git docs-2.0 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files + git fetch https://github.com/grafana/grafana.git docs-1.x && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files echo "$(GIT_BRANCH)" > GIT_BRANCH echo "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . From 9ae3d66da72dc53a7eae2430524292bace801b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:18:11 +0200 Subject: [PATCH 088/398] Fixed docs, screencasts page --- docs/sources/guides/screencasts.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/sources/guides/screencasts.md b/docs/sources/guides/screencasts.md index 6c09e5b26af..d8c605a4245 100644 --- a/docs/sources/guides/screencasts.md +++ b/docs/sources/guides/screencasts.md @@ -15,10 +15,9 @@ no_toc: true

    Episode 2 - Templated Graphite Queries

    - +
    -
    @@ -34,7 +33,6 @@ no_toc: true
    -
    @@ -50,7 +48,6 @@ no_toc: true
    -
    From 74a8fa61f2217c6a63d9d08394c8086173632c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 10:33:37 +0200 Subject: [PATCH 089/398] Merged with 2.0 docs and restored some stuff --- conf/defaults.ini | 6 +++--- conf/sample.ini | 2 ++ docs/sources/installation/configuration.md | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 7345f18520e..4fef290b908 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -137,19 +137,20 @@ org_role = Viewer #################################### Github Auth ########################## [auth.github] enabled = false +allow_sign_up = false client_id = some_id client_secret = some_secret scopes = user:email -team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user +team_ids = allowed_domains = -allow_sign_up = false #################################### Google Auth ########################## [auth.google] enabled = false +allow_sign_up = false client_id = some_client_id client_secret = some_client_secret scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email @@ -157,7 +158,6 @@ auth_url = https://accounts.google.com/o/oauth2/auth token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = -allow_sign_up = false #################################### Logging ########################## [log] diff --git a/conf/sample.ini b/conf/sample.ini index 4ebab72d1af..62d5c2a09ce 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -136,6 +136,7 @@ #################################### Github Auth ########################## [auth.github] ;enabled = false +;allow_sign_up = false ;client_id = some_id ;client_secret = some_secret ;scopes = user:email @@ -148,6 +149,7 @@ #################################### Google Auth ########################## [auth.google] ;enabled = false +;allow_sign_up = false ;client_id = some_client_id ;client_secret = some_client_secret ;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f55561e6668..ace8c57184f 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -182,6 +182,7 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token allow_sign_up = false + team_ids = Restart the grafana backend. You should now see a github login button on the login page. You can now login or signup with your github accounts. @@ -189,6 +190,21 @@ now login or signup with your github accounts. You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is set to true, any user successfully authenticating via github auth will be automatically signed up. +### team_ids +Require an active team membership for at least one of the given teams on GitHub. +If the authenticated user isn't a member of at least one the teams they will not +be able to register or authenticate with your Grafana instance. Example: + + [auth.github] + enabled = true + client_id = YOUR_GITHUB_APP_CLIENT_ID + client_secret = YOUR_GITHUB_APP_CLIENT_SECRET + scopes = user:email + team_ids = 150,300 + auth_url = https://github.com/login/oauth/authorize + token_url = https://github.com/login/oauth/access_token + allow_sign_up = false + ## [auth.google] You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project). When you create the project you will need to specify a callback URL. Specify this as callback: 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 090/398] 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 091/398] 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 d10ce90936cc6f4c53e584f97af39c300e7f6066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Apr 2015 15:50:47 +0200 Subject: [PATCH 092/398] Fixed XSS issue with file based dashboards, was really casued by an issue with alertSrv accepting html in message alerts --- public/app/routes/dashLoadControllers.js | 2 +- public/app/services/alertSrv.js | 2 +- public/views/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/routes/dashLoadControllers.js b/public/app/routes/dashLoadControllers.js index b19b0c9664f..570ce2b18c1 100644 --- a/public/app/routes/dashLoadControllers.js +++ b/public/app/routes/dashLoadControllers.js @@ -76,7 +76,7 @@ function (angular, _, kbn, moment, $) { } return result.data; },function() { - $scope.appEvent('alert-error', ["Dashboard load failed", "Could not load dashboards/"+file+". Please make sure it exists"]); + $scope.appEvent('alert-error', ["Dashboard load failed", "Could not load "+file+". Please make sure it exists"]); return false; }); }; diff --git a/public/app/services/alertSrv.js b/public/app/services/alertSrv.js index d38c1f4aecc..4a8ef273523 100644 --- a/public/app/services/alertSrv.js +++ b/public/app/services/alertSrv.js @@ -29,7 +29,7 @@ function (angular, _) { this.set = function(title,text,severity,timeout) { var newAlert = { title: title || '', - text: $sce.trustAsHtml(text || ''), + text: text || '', severity: severity || 'info', }; diff --git a/public/views/index.html b/public/views/index.html index d5dfab06a64..8fb4a93bba6 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -35,7 +35,7 @@
    {{alert.title}}
    -
    +
    From 0a2b03ba52f4cee22d0bdcb875d811ea7fc4ba48 Mon Sep 17 00:00:00 2001 From: Morton Fox Date: Wed, 29 Apr 2015 16:35:03 -0400 Subject: [PATCH 093/398] Fix the first CLA link. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd178eddbdd..5f0fb88b951 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ You only need to add the options you want to override. Config files are applied 2. dev.ini (if found) 3. custom.ini -## Create a pull requests -Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html).## Contribute +## Create a pull request +Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html). +## Contribute If you have any idea for an improvement or found a bug do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about! From a8b2074fc6045f25f8538f942e5cc39dfde71d55 Mon Sep 17 00:00:00 2001 From: Morton Fox Date: Wed, 29 Apr 2015 16:35:03 -0400 Subject: [PATCH 094/398] Fix the first CLA link. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd178eddbdd..5f0fb88b951 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ You only need to add the options you want to override. Config files are applied 2. dev.ini (if found) 3. custom.ini -## Create a pull requests -Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html).## Contribute +## Create a pull request +Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html). +## Contribute If you have any idea for an improvement or found a bug do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about! 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 095/398] 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 096/398] 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 097/398] 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 098/398] 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; From 95fcddcd952fa8c497c1bda703f706e5feba339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 08:07:45 +0200 Subject: [PATCH 099/398] More work on panel & row repeats, #1888, updated changelog --- CHANGELOG.md | 4 ++ public/app/directives/all.js | 2 +- ...aramSelector.js => variableValueSelect.js} | 0 .../features/dashboard/dynamicDashboardSrv.js | 29 +++++----- public/app/partials/roweditor.html | 2 +- .../test/specs/dynamicDashboardSrv-specs.js | 57 +++++++++++++++++++ 6 files changed, 77 insertions(+), 17 deletions(-) rename public/app/directives/{templateParamSelector.js => variableValueSelect.js} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70aaa67ab22..d0358f7e572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 2.1.0 (unreleased - master branch) +**New dashboard features** +- [Issue #1144](https://github.com/grafana/grafana/issues/1144). Templating: You can now select multiple template variables values at the same time. +- [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value + **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 3ef0b669d6c..4ab92d111d6 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -11,7 +11,7 @@ define([ './spectrumPicker', './bootstrap-tagsinput', './bodyClass', - './templateParamSelector', + './variableValueSelect', './graphiteSegment', './grafanaVersionCheck', './dropdown.typeahead', diff --git a/public/app/directives/templateParamSelector.js b/public/app/directives/variableValueSelect.js similarity index 100% rename from public/app/directives/templateParamSelector.js rename to public/app/directives/variableValueSelect.js diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index cfa824e3402..dcb4e31340c 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -28,7 +28,17 @@ function (angular, _) { for (i = 0; i < this.dashboard.rows.length; i++) { row = this.dashboard.rows[i]; - // repeat panels first + // handle row repeats + if (row.repeat) { + this.repeatRow(row); + } + // clean up old left overs + else if (row.repeatRowId && row.repeatIteration !== this.iteration) { + this.dashboard.rows.splice(i, 1); + i = i - 1; + } + + // repeat panels for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.repeat) { @@ -40,16 +50,6 @@ function (angular, _) { j = j - 1; } } - - // handle row repeats - if (row.repeat) { - this.repeatRow(row); - } - // clean up old left overs - else if (row.repeatRowId && row.repeatIteration !== this.iteration) { - this.dashboard.rows.splice(i, 1); - i = i - 1; - } } }; @@ -108,7 +108,7 @@ function (angular, _) { for (i = 0; i < copy.panels.length; i++) { panel = copy.panels[i]; - panel.scopedVars = panel.scopedVars || {}; + panel.scopedVars = {}; panel.scopedVars[variable.name] = option; } }); @@ -139,7 +139,7 @@ function (angular, _) { // save id tmpId = clone.id; // copy properties from source - angular.extend(clone, sourcePanel); + angular.copy(sourcePanel, clone); // restore id clone.id = tmpId; clone.repeatIteration = this.iteration; @@ -162,11 +162,10 @@ function (angular, _) { _.each(selected, function(option, index) { var copy = self.getPanelClone(panel, row, index); - copy.scopedVars = {}; + copy.scopedVars = copy.scopedVars || {}; copy.scopedVars[variable.name] = option; }); }; }); - }); diff --git a/public/app/partials/roweditor.html b/public/app/partials/roweditor.html index 37e258a9b89..5780d624302 100644 --- a/public/app/partials/roweditor.html +++ b/public/app/partials/roweditor.html @@ -34,7 +34,7 @@
  • -
  • +
  • diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 08a2f1af561..905b8896cd4 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -177,4 +177,61 @@ define([ }); }); }); + + dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + repeat: 'servers', + panels: [{id: 2, repeat: 'metric'}] + }); + 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}, + ] + }); + dash.templating.list.push({ + name: 'metric', + current: { text: 'm1, m2', value: ['m1', 'm2'] }, + options: [ + {text: 'm1', value: 'm1', selected: true}, + {text: 'm2', value: 'm2', selected: true}, + ] + }); + }); + + it('should repeat row one time', function() { + expect(ctx.rows.length).to.be(2); + }); + + it('should repeat panel on both rows', function() { + expect(ctx.rows[0].panels.length).to.be(2); + expect(ctx.rows[1].panels.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].repeat).to.be('servers'); + }); + + 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 59fcd3914d4254625da41833f92c658e6ffb43e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 08:08:31 +0200 Subject: [PATCH 100/398] corrected master version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64e393616f6..8785632eaee 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "2.0.3-pre1", + "version": "2.1.0-pre1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" From 5de92309243ca3c100a49bdf2e32a1992d0ca235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 08:40:13 +0200 Subject: [PATCH 101/398] Security: New config option to disable the use of gravatar for profile images, Closes #1891 --- CHANGELOG.md | 1 + conf/defaults.ini | 3 +++ conf/sample.ini | 3 +++ docs/sources/installation/configuration.md | 3 +++ pkg/api/index.go | 4 ++++ pkg/setting/setting.go | 3 +++ .../plugins/datasource/influxdb_08/datasource.js | 4 ++-- public/img/user_profile.png | Bin 0 -> 1486 bytes 8 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 public/img/user_profile.png diff --git a/CHANGELOG.md b/CHANGELOG.md index d0358f7e572..517fa82779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski +- [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images # 2.0.3 (unreleased - 2.0.x branch) diff --git a/conf/defaults.ini b/conf/defaults.ini index 4fef290b908..891c04920e1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -109,6 +109,9 @@ login_remember_days = 7 cookie_username = grafana_user cookie_remember_name = grafana_remember +# disable gravatar profile images +disable_gravatar = false + #################################### Users #################################### [users] # disable user signup / registration diff --git a/conf/sample.ini b/conf/sample.ini index 62d5c2a09ce..aa333cdc5de 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -108,6 +108,9 @@ ;cookie_username = grafana_user ;cookie_remember_name = grafana_remember +# disable gravatar profile images +;disable_gravatar = false + #################################### Users #################################### [users] # disable user signup / registration diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index ace8c57184f..7ccee50fbc9 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -134,6 +134,9 @@ The number of days the keep me logged in / remember me cookie lasts. ### secret_key Used for signing keep me logged in / remember me cookies. +### disable_gravatar +Set to true to disable the use of Gravatar for user profile images. Default is `false`. +
    ## [user] diff --git a/pkg/api/index.go b/pkg/api/index.go index 86a5e3f1882..386bb3351df 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -25,6 +25,10 @@ func setIndexViewData(c *middleware.Context) error { IsGrafanaAdmin: c.IsGrafanaAdmin, } + if setting.DisableGravatar { + currentUser.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png" + } + if len(currentUser.Name) == 0 { currentUser.Name = currentUser.Login } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 46f98e32efe..520267fb871 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -71,6 +71,7 @@ var ( LogInRememberDays int CookieUserName string CookieRememberName string + DisableGravatar bool // User settings AllowUserSignUp bool @@ -358,6 +359,8 @@ func NewConfigContext(args *CommandLineArgs) { LogInRememberDays = security.Key("login_remember_days").MustInt() CookieUserName = security.Key("cookie_username").String() CookieRememberName = security.Key("cookie_remember_name").String() + DisableGravatar = security.Key("disable_gravatar").MustBool(true) + // admin AdminUser = security.Key("admin_user").String() AdminPassword = security.Key("admin_password").String() diff --git a/public/app/plugins/datasource/influxdb_08/datasource.js b/public/app/plugins/datasource/influxdb_08/datasource.js index 7462bdb41b9..8ff01c8553f 100644 --- a/public/app/plugins/datasource/influxdb_08/datasource.js +++ b/public/app/plugins/datasource/influxdb_08/datasource.js @@ -42,9 +42,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { query = query.replace(/\$interval/g, (target.interval || options.interval)); // replace templated variables - query = templateSrv.replace(query); + query = templateSrv.replace(query, options.scopedVars); - var alias = target.alias ? templateSrv.replace(target.alias) : ''; + var alias = target.alias ? templateSrv.replace(target.alias, options.scopedVars) : ''; var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField); return this._seriesQuery(query).then(handleResponse); diff --git a/public/img/user_profile.png b/public/img/user_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..bc492598e4fa5982c4bee100a50d35865d77a610 GIT binary patch literal 1486 zcmex=_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(6Oj-S5fuR$!pIEN!@|nR z%E~Fi%grl7GWdUhL6CzXiXn=bQHg;`kdaxC@&6G9d7vj*8Nq-73K*GyZe(NU;N;>4 zD%dK(z{JSR%*4VBay3wOEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJOE?_w-yD~F_amQ+_y1B)x?zv`X3r?%#6LtUj zAVl=_eJ&fjX;scyMQV9^og5y&H#YZQX{y&^n0$CYx5-;8mC1qs8T@y>3YnxF{Kop9 z{nXOH7qfCaH|ppH+V4^fu%8;csKIh?`onjftB>7(u{zW6rS68C{Rax_wLaO4R``8B zw7p65qYD3gtx|dI{ui~YUvQoIsMaptv8B4(lHtx_!;SKV$DRLGr#0A>KkJ(0aZ6KH zwrtm=C344RJMcd130|>)smtf$tp>4utsiD-{YduYviPl&{_&Rauj}V6*Ju5RU8}bA z;g)SWHouLZZu)+!GWf4O7sH#1#UGX|Y7qYbq*l$^wv?}b*_}8)hb|GF+VhD&=4mh= ztO@Cur*_qeaZ9Y{e};upr&sFb$-R4)`NVRPo8Iv|b7pn#WzZFwxu`*?=izkS-(D?!Bu@D99&bkP6;5Z^PZrFv`e%E5 z;{yKq#%|fkNB&9d{;>Dw_MOs?b7m}y-}U&HeAaQb2A)gLdM*_@aCaGcN8e++=(h7w zw1-SyEB|Z_+pCqw9~Ib3F?^Zd_D?95-zWO!<-b->i%&c=c_sd;YW}~iWs%ROizGD& zn=H84W$3_`aaLln2;=lewruy0%!=7o+`#$F(t2^3(jKONhjPunWk+65xoGGn_h{eb zqm|*uctsp5vsWH7P?HN;!v9FVev%U@5b zpRnF|ebf&#`$xXD{~2PB|7+4 Date: Fri, 1 May 2015 08:52:34 +0200 Subject: [PATCH 102/398] Added new graphite function spec, weightedAverage, Closes #1879 --- public/app/plugins/datasource/graphite/gfunc.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/app/plugins/datasource/graphite/gfunc.js b/public/app/plugins/datasource/graphite/gfunc.js index 7190fbf0295..063fc473f88 100644 --- a/public/app/plugins/datasource/graphite/gfunc.js +++ b/public/app/plugins/datasource/graphite/gfunc.js @@ -530,6 +530,16 @@ function (_, $) { defaultParams: [10] }); + addFuncDef({ + name: 'weightedAverage', + category: categories.Filter, + params: [ + { name: 'other', type: 'value_or_series', optional: true }, + { name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] }, + ], + defaultParams: ['#A', 4] + }); + addFuncDef({ name: 'movingMedian', category: categories.Filter, From e771d8e944b5fe80d27e25abaec39bd5258b229d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 09:48:07 +0200 Subject: [PATCH 103/398] Organization: You can now update the organization user role directly (without removing and readding the organization user). Closes #1899 --- CHANGELOG.md | 3 +++ pkg/api/api.go | 1 + pkg/api/org_users.go | 17 ++++++++++++++++ pkg/models/org_user.go | 9 +++++++++ pkg/services/sqlstore/org_test.go | 13 ++++++++++++ pkg/services/sqlstore/org_users.go | 20 +++++++++++++++++++ public/app/features/org/orgUsersCtrl.js | 4 ++++ .../app/features/org/partials/orgUsers.html | 5 +++-- public/app/services/backendSrv.js | 4 ++++ 9 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517fa82779b..22e4f5482b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - [Issue #1144](https://github.com/grafana/grafana/issues/1144). Templating: You can now select multiple template variables values at the same time. - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value +**User or Organization admin** +- [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). + **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images diff --git a/pkg/api/api.go b/pkg/api/api.go index 6482beb075c..d281d94419f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -71,6 +71,7 @@ func Register(r *macaron.Macaron) { r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg) r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser) r.Get("/users", GetOrgUsers) + r.Patch("/users/:id", bind(m.UpdateOrgUserCommand{}), UpdateOrgUser) r.Delete("/users/:id", RemoveOrgUser) }, reqAccountAdmin) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 123ba08c4c7..8a372c791b9 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -48,6 +48,23 @@ func GetOrgUsers(c *middleware.Context) { c.JSON(200, query.Result) } +func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) { + if !cmd.Role.IsValid() { + c.JsonApiErr(400, "Invalid role specified", nil) + return + } + + cmd.UserId = c.ParamsInt64(":id") + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed update org user", err) + return + } + + c.JsonOK("Organization user updated") +} + func RemoveOrgUser(c *middleware.Context) { userId := c.ParamsInt64(":id") diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 811d02e1afe..3e40fd24b68 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -9,6 +9,7 @@ import ( var ( ErrInvalidRoleType = errors.New("Invalid role type") ErrLastOrgAdmin = errors.New("Cannot remove last organization admin") + ErrOrgUserNotFound = errors.New("Cannot find the organization user") ) type RoleType string @@ -24,6 +25,7 @@ func (r RoleType) IsValid() bool { } type OrgUser struct { + Id int64 OrgId int64 UserId int64 Role RoleType @@ -47,6 +49,13 @@ type AddOrgUserCommand struct { UserId int64 `json:"-"` } +type UpdateOrgUserCommand struct { + Role RoleType `json:"role" binding:"Required"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` +} + // ---------------------- // QUERIES diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 0e20919709b..7d98da076e0 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -80,6 +80,19 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldBeNil) }) + Convey("Can update org user role", func() { + updateCmd := m.UpdateOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, Role: m.ROLE_ADMIN} + err = UpdateOrgUser(&updateCmd) + So(err, ShouldBeNil) + + orgUsersQuery := m.GetOrgUsersQuery{OrgId: ac1.OrgId} + err = GetOrgUsers(&orgUsersQuery) + So(err, ShouldBeNil) + + So(orgUsersQuery.Result[1].Role, ShouldEqual, m.ROLE_ADMIN) + + }) + Convey("Can get logged in user projection", func() { query := m.GetSignedInUserQuery{UserId: ac2.Id} err := GetSignedInUser(&query) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index eaca01ce12c..3502cd97d60 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -14,6 +14,7 @@ func init() { bus.AddHandler("sql", AddOrgUser) bus.AddHandler("sql", RemoveOrgUser) bus.AddHandler("sql", GetOrgUsers) + bus.AddHandler("sql", UpdateOrgUser) } func AddOrgUser(cmd *m.AddOrgUserCommand) error { @@ -32,6 +33,25 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error { }) } +func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var orgUser m.OrgUser + exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&orgUser) + if err != nil { + return err + } + + if !exists { + return m.ErrOrgUserNotFound + } + + orgUser.Role = cmd.Role + orgUser.Updated = time.Now() + _, err = sess.Id(orgUser.Id).Update(&orgUser) + return err + }) +} + func GetOrgUsers(query *m.GetOrgUsersQuery) error { query.Result = make([]*m.OrgUserDTO, 0) sess := x.Table("org_user") diff --git a/public/app/features/org/orgUsersCtrl.js b/public/app/features/org/orgUsersCtrl.js index d679a8dab98..9623e2566a7 100644 --- a/public/app/features/org/orgUsersCtrl.js +++ b/public/app/features/org/orgUsersCtrl.js @@ -23,6 +23,10 @@ function (angular) { }); }; + $scope.updateOrgUser = function(user) { + backendSrv.patch('/api/org/users/' + user.userId, user); + }; + $scope.removeUser = function(user) { backendSrv.delete('/api/org/users/' + user.userId).then($scope.get); }; diff --git a/public/app/features/org/partials/orgUsers.html b/public/app/features/org/partials/orgUsers.html index c14a44efef7..8b1829437c7 100644 --- a/public/app/features/org/partials/orgUsers.html +++ b/public/app/features/org/partials/orgUsers.html @@ -35,7 +35,7 @@
    - +
    @@ -46,7 +46,8 @@ From 62e8841e8cf2f63ec3a18eb6a6dd9c16d3b57a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 17:54:12 +0200 Subject: [PATCH 190/398] Fixed spelling of Peta unit Quadr --- public/app/components/kbn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index ef1e45c23d3..15a4c7637c3 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -384,7 +384,7 @@ function($, _, moment) { kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); - kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']); + kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']); kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); kbn.valueFormats.amp = kbn.formatFuncCreator(1000, [' A', ' kA', ' MA', ' GA', ' TA', ' PA', ' EA', ' ZA', ' YA']); kbn.valueFormats.volt = kbn.formatFuncCreator(1000, [' V', ' kV', ' MV', ' GV', ' TV', ' PV', ' EV', ' ZV', ' YV']); From fbc6bb21123d3bb97efe61c19abe7896e3a741a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 19:06:19 +0200 Subject: [PATCH 191/398] More refactoring of user http api, trying to reuse handlers for sign in user and admin operations --- pkg/api/admin_users.go | 46 ------------------- pkg/api/api.go | 7 ++- pkg/api/common.go | 17 +++++-- pkg/api/user.go | 26 +++++++++-- pkg/services/sqlstore/user.go | 9 ++-- .../app/features/admin/adminEditUserCtrl.js | 4 +- 6 files changed, 45 insertions(+), 64 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index f7e8fca2b5e..23cfc826ed2 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -19,26 +19,6 @@ func AdminSearchUsers(c *middleware.Context) { c.JSON(200, query.Result) } -func AdminGetUser(c *middleware.Context) { - userId := c.ParamsInt64(":id") - - query := m.GetUserByIdQuery{Id: userId} - - if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to fetch user", err) - return - } - - result := dtos.AdminUserListItem{ - Name: query.Result.Name, - Email: query.Result.Email, - Login: query.Result.Login, - IsGrafanaAdmin: query.Result.IsAdmin, - } - - c.JSON(200, result) -} - func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { cmd := m.CreateUserCommand{ Login: form.Login, @@ -70,32 +50,6 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { c.JsonOK("User created") } -func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { - userId := c.ParamsInt64(":id") - - cmd := m.UpdateUserCommand{ - UserId: userId, - Login: form.Login, - Email: form.Email, - Name: form.Name, - } - - if len(cmd.Login) == 0 { - cmd.Login = cmd.Email - if len(cmd.Login) == 0 { - c.JsonApiErr(400, "Validation error, need specify either username or email", nil) - return - } - } - - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "failed to update user", err) - return - } - - c.JsonOK("User updated") -} - func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { userId := c.ParamsInt64(":id") diff --git a/pkg/api/api.go b/pkg/api/api.go index cfc1f50b98e..a84c753c7ba 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -56,7 +56,7 @@ func Register(r *macaron.Macaron) { // user r.Group("/user", func() { r.Get("/", wrap(GetSignedInUser)) - r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) + r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser)) r.Post("/using/:id", UserSetUsingOrg) r.Get("/orgs", wrap(GetSignedInUserOrgList)) r.Post("/stars/dashboard/:id", StarDashboard) @@ -66,8 +66,9 @@ func Register(r *macaron.Macaron) { // users r.Group("/users", func() { - r.Get("/:id/", wrap(GetUserById)) + r.Get("/:id", wrap(GetUserById)) r.Get("/:id/org", wrap(GetUserOrgList)) + r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) // account @@ -122,9 +123,7 @@ func Register(r *macaron.Macaron) { r.Group("/api/admin", func() { r.Get("/settings", AdminGetSettings) r.Get("/users", AdminSearchUsers) - r.Get("/users/:id", AdminGetUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) - r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) r.Delete("/users/:id", AdminDeleteUser) diff --git a/pkg/api/common.go b/pkg/api/common.go index 8757318159e..4d8b3c28032 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -26,12 +26,17 @@ type NormalResponse struct { header http.Header } -func wrap(action func(c *middleware.Context) Response) macaron.Handler { +func wrap(action interface{}) macaron.Handler { + return func(c *middleware.Context) { - res := action(c) - if res == nil { + var res Response + val, err := c.Invoke(action) + if err == nil && val != nil && len(val) > 0 { + res = val[0].Interface().(Response) + } else { res = ServerError } + res.WriteTo(c.Resp) } } @@ -64,6 +69,12 @@ func Json(status int, body interface{}) *NormalResponse { return Respond(status, body).Header("Content-Type", "application/json") } +func ApiSuccess(message string) *NormalResponse { + resp := make(map[string]interface{}) + resp["message"] = message + return Respond(200, resp) +} + func ApiError(status int, message string, err error) *NormalResponse { resp := make(map[string]interface{}) diff --git a/pkg/api/user.go b/pkg/api/user.go index e7cc8ff0366..1b5654ce1f0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -27,15 +27,31 @@ func getUserUserProfile(userId int64) Response { return Json(200, query.Result) } -func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { +// POST /api/user +func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { cmd.UserId = c.UserId + return handleUpdateUser(cmd) +} - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(400, "Failed to update user", err) - return +// POST /api/users/:id +func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { + cmd.UserId = c.ParamsInt64(":id") + return handleUpdateUser(cmd) +} + +func handleUpdateUser(cmd m.UpdateUserCommand) Response { + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + return ApiError(400, "Validation error, need specify either username or email", nil) + } } - c.JsonOK("User updated") + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "failed to update user", err) + } + + return ApiSuccess("User updated") } // GET /api/user/orgs diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 663496427ba..b74422b69ca 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -231,10 +231,11 @@ func GetUserProfile(query *m.GetUserProfileQuery) error { } query.Result = m.UserProfileDTO{ - Name: user.Name, - Email: user.Email, - Login: user.Login, - Theme: user.Theme, + Name: user.Name, + Email: user.Email, + Login: user.Login, + Theme: user.Theme, + IsGrafanaAdmin: user.IsAdmin, } return err diff --git a/public/app/features/admin/adminEditUserCtrl.js b/public/app/features/admin/adminEditUserCtrl.js index 19deac532ea..72a7dba0229 100644 --- a/public/app/features/admin/adminEditUserCtrl.js +++ b/public/app/features/admin/adminEditUserCtrl.js @@ -17,7 +17,7 @@ function (angular) { }; $scope.getUser = function(id) { - backendSrv.get('/api/admin/users/' + id).then(function(user) { + backendSrv.get('/api/users/' + id).then(function(user) { $scope.user = user; $scope.user_id = id; $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; @@ -52,7 +52,7 @@ function (angular) { $scope.update = function() { if (!$scope.userForm.$valid) { return; } - backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() { + backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(function() { $location.path('/admin/users'); }); }; From f81bde5643722ebb3e8290e8208347d288f4d935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 21:23:40 +0200 Subject: [PATCH 192/398] Refactoring some api handlers to use the new Response return object --- pkg/api/api.go | 6 ++--- pkg/api/apikey.go | 28 ++++++++++------------- public/app/features/org/orgApiKeysCtrl.js | 2 ++ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index a84c753c7ba..0503ac61a3c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -84,9 +84,9 @@ func Register(r *macaron.Macaron) { // auth api keys r.Group("/auth/keys", func() { - r.Get("/", GetApiKeys) - r.Post("/", bind(m.AddApiKeyCommand{}), AddApiKey) - r.Delete("/:id", DeleteApiKey) + r.Get("/", wrap(GetApiKeys)) + r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) + r.Delete("/:id", wrap(DeleteApiKey)) }, reqAccountAdmin) // Data sources diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 237fdc48ab0..b2097104aba 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -8,12 +8,11 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func GetApiKeys(c *middleware.Context) { +func GetApiKeys(c *middleware.Context) Response { query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to list api keys", err) - return + return ApiError(500, "Failed to list api keys", err) } result := make([]*m.ApiKeyDTO, len(query.Result)) @@ -24,27 +23,26 @@ func GetApiKeys(c *middleware.Context) { Role: t.Role, } } - c.JSON(200, result) + + return Json(200, result) } -func DeleteApiKey(c *middleware.Context) { +func DeleteApiKey(c *middleware.Context) Response { id := c.ParamsInt64(":id") cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} err := bus.Dispatch(cmd) if err != nil { - c.JsonApiErr(500, "Failed to delete API key", err) - return + return ApiError(500, "Failed to delete API key", err) } - c.JsonOK("API key deleted") + return ApiSuccess("API key deleted") } -func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { +func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response { if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return + return ApiError(400, "Invalid role specified", nil) } cmd.OrgId = c.OrgId @@ -53,14 +51,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { cmd.Key = newKeyInfo.HashedKey if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to add API key", err) - return + return ApiError(500, "Failed to add API key", err) } result := &dtos.NewApiKeyResult{ Name: cmd.Result.Name, - Key: newKeyInfo.ClientSecret, - } + Key: newKeyInfo.ClientSecret} - c.JSON(200, result) + return Json(200, result) } diff --git a/public/app/features/org/orgApiKeysCtrl.js b/public/app/features/org/orgApiKeysCtrl.js index a8b05155401..918f57b42e8 100644 --- a/public/app/features/org/orgApiKeysCtrl.js +++ b/public/app/features/org/orgApiKeysCtrl.js @@ -35,6 +35,8 @@ function (angular) { src: './app/features/org/partials/apikeyModal.html', scope: modalScope }); + + $scope.getTokens(); }); }; From bfe6d5434e70f5ea1c1af6acbb568d5878ab7843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 08:46:45 +0200 Subject: [PATCH 193/398] Fixed placeholder text in templating editor --- public/app/features/templating/partials/editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 11769e34dfc..a0549414b6a 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -65,7 +65,7 @@ Name
  • - +
  • Type @@ -139,7 +139,7 @@ Query
  • - +
  • From bf9e51928df97ff0bc2895c869697648dd660def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 09:02:37 +0200 Subject: [PATCH 194/398] Fix to signed in user when user <-> org link is gone --- pkg/services/sqlstore/user.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b74422b69ca..9f79081783b 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -283,6 +283,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { return m.ErrUserNotFound } + if user.OrgRole == "" { + user.OrgId = -1 + user.OrgName = "Org missing" + } + query.Result = &user return err } From 74bf1f23fb8e217e82aff0c9d7b5e697e0e6a9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 09:09:21 +0200 Subject: [PATCH 195/398] Small progress on #2014 --- pkg/api/api.go | 2 +- public/app/features/admin/adminEditUserCtrl.js | 7 +++++++ .../app/features/admin/partials/edit_user.html | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0503ac61a3c..f45da26d9c2 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -67,7 +67,7 @@ func Register(r *macaron.Macaron) { // users r.Group("/users", func() { r.Get("/:id", wrap(GetUserById)) - r.Get("/:id/org", wrap(GetUserOrgList)) + r.Get("/:id/orgs", wrap(GetUserOrgList)) r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) diff --git a/public/app/features/admin/adminEditUserCtrl.js b/public/app/features/admin/adminEditUserCtrl.js index 72a7dba0229..dc8839f0f84 100644 --- a/public/app/features/admin/adminEditUserCtrl.js +++ b/public/app/features/admin/adminEditUserCtrl.js @@ -13,6 +13,7 @@ function (angular) { $scope.init = function() { if ($routeParams.id) { $scope.getUser($routeParams.id); + $scope.getUserOrgs($routeParams.id); } }; @@ -49,6 +50,12 @@ function (angular) { }); }; + $scope.getUserOrgs = function(id) { + backendSrv.get('/api/users/' + id + '/orgs').then(function(orgs) { + $scope.orgs = orgs; + }); + }; + $scope.update = function() { if (!$scope.userForm.$valid) { return; } diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index c82d7705b8d..712aee26d5d 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -94,5 +94,21 @@
    +

    + Organizations +

    + +
    Login Email{{user.login}} {{user.email}} - {{user.role}} + diff --git a/public/app/services/backendSrv.js b/public/app/services/backendSrv.js index c208fe3388e..7424d8fcd83 100644 --- a/public/app/services/backendSrv.js +++ b/public/app/services/backendSrv.js @@ -23,6 +23,10 @@ function (angular, _, config) { return this.request({ method: 'POST', url: url, data: data }); }; + this.patch = function(url, data) { + return this.request({ method: 'PATCH', url: url, data: data }); + }; + this.put = function(url, data) { return this.request({ method: 'PUT', url: url, data: data }); }; From 7a8851c5ab75a4c1c0154a7c31fa6681a8c29fe5 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Fri, 1 May 2015 17:02:00 +0800 Subject: [PATCH 104/398] default sidemenu to open #54 --- public/app/services/contextSrv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index 99bbcccf156..700b7f7390c 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -45,7 +45,7 @@ function (angular, _, store, config) { this.user = new User(); this.isSignedIn = this.user.isSignedIn; this.isGrafanaAdmin = this.user.isGrafanaAdmin; - this.sidemenu = store.getBool('grafana.sidemenu'); + this.sidemenu = store.getBool('grafana.sidemenu', true); this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); }); }); From e7ac367392df731348dfdac079487d8af8436443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 11:22:21 +0200 Subject: [PATCH 105/398] SignUp: password strength meter updated, Fixes #1892 --- public/app/controllers/loginCtrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index c8856df0690..cda5a085fd3 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -67,7 +67,7 @@ function (angular, config) { $scope.strengthClass = "password-strength-bad"; return; } - if (newValue.length <= 6) { + if (newValue.length <= 8) { $scope.strengthText = "strength: you can do better."; $scope.strengthClass = "password-strength-ok"; return; From d1e9b6d6ae952b466c2e4a139113568e0f54a41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 11:55:59 +0200 Subject: [PATCH 106/398] Began work on auth_proxy feature (#1932), and began work on testing http api, and auth middleware --- conf/defaults.ini | 7 ++++++ pkg/cmd/web.go | 2 +- pkg/middleware/middleware_test.go | 40 +++++++++++++++++++++++++++++++ pkg/middleware/session.go | 34 ++++++++++++++++++++++---- pkg/setting/setting.go | 13 ++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 pkg/middleware/middleware_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 891c04920e1..88218240bac 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -162,6 +162,13 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = +#################################### Auth Proxy ########################## +[auth.proxy] +enabled = false; +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true + #################################### Logging ########################## [log] # Either "console", "file", default is "console" diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 7b6014aeb7e..a95d2af2498 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -41,7 +41,7 @@ func newMacaron() *macaron.Macaron { })) m.Use(middleware.GetContextHandler()) - m.Use(middleware.Sessioner(setting.SessionOptions)) + m.Use(middleware.Sessioner(&setting.SessionOptions)) return m } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go new file mode 100644 index 00000000000..9be76121718 --- /dev/null +++ b/pkg/middleware/middleware_test.go @@ -0,0 +1,40 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Unknwon/macaron" + "github.com/macaron-contrib/session" + . "github.com/smartystreets/goconvey/convey" +) + +func TestMiddlewareContext(t *testing.T) { + + Convey("Given grafana context", t, func() { + m := macaron.New() + m.Use(GetContextHandler()) + m.Use(Sessioner(&session.Options{})) + + var context *Context + + m.Get("/", func(c *Context) { + context = c + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + + m.ServeHTTP(resp, req) + + Convey("Should be able to get grafana context in handlers", func() { + So(context, ShouldNotBeNil) + }) + + Convey("should return 200", func() { + So(resp.Code, ShouldEqual, 200) + }) + }) +} diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 71f87b343ff..c0a12cab781 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -16,17 +16,43 @@ const ( ) var sessionManager *session.Manager -var sessionOptions session.Options +var sessionOptions *session.Options func startSessionGC() { sessionManager.GC() time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC) } -func Sessioner(options session.Options) macaron.Handler { +func prepareOptions(opt *session.Options) *session.Options { + if len(opt.Provider) == 0 { + opt.Provider = "memory" + } + if len(opt.ProviderConfig) == 0 { + opt.ProviderConfig = "data/sessions" + } + if len(opt.CookieName) == 0 { + opt.CookieName = "grafana_sess" + } + if len(opt.CookiePath) == 0 { + opt.CookiePath = "/" + } + if opt.Gclifetime == 0 { + opt.Gclifetime = 3600 + } + if opt.Maxlifetime == 0 { + opt.Maxlifetime = opt.Gclifetime + } + if opt.IDLength == 0 { + opt.IDLength = 16 + } + + return opt +} + +func Sessioner(options *session.Options) macaron.Handler { var err error - sessionOptions = options - sessionManager, err = session.NewManager(options.Provider, options) + sessionOptions = prepareOptions(options) + sessionManager, err = session.NewManager(options.Provider, *options) if err != nil { panic(err) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 520267fb871..b451aad634f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -87,6 +87,12 @@ var ( AnonymousOrgName string AnonymousOrgRole string + // Auth proxy settings + AuthProxyEnabled bool + AuthProxyHeaderName string + AuthProxyHeaderProperty string + AuthProxyAutoSignUp bool + // Session settings. SessionOptions session.Options @@ -376,6 +382,13 @@ func NewConfigContext(args *CommandLineArgs) { AnonymousOrgName = Cfg.Section("auth.anonymous").Key("org_name").String() AnonymousOrgRole = Cfg.Section("auth.anonymous").Key("org_role").String() + // auth proxy + authProxy := Cfg.Section("auth.proxy") + AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) + AuthProxyHeaderName = authProxy.Key("header_name").String() + AuthProxyHeaderProperty = authProxy.Key("header_property").String() + AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + // PhantomJS rendering ImagesDir = filepath.Join(DataPath, "png") PhantomDir = filepath.Join(HomePath, "vendor/phantomjs") From 8d081a081df53432a49fe76e45e5070c5bdbb77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 15:01:31 +0200 Subject: [PATCH 107/398] Fixed configuration docs issue, Fixes #1924 --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f55561e6668..32c41ca332e 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -135,7 +135,7 @@ The number of days the keep me logged in / remember me cookie lasts. Used for signing keep me logged in / remember me cookies.
    -## [user] +## [users] ### allow_sign_up Set to `false` to prohibit users from being able to sign up / create user accounts. Defaults to `true`. From cb8110cd48edf4445b4679281b207fc5190ac13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 16:23:36 +0200 Subject: [PATCH 108/398] Refactoring, worked on middleware unit tests, and began thinking about api unit tests, #1921 --- pkg/api/api_test.go | 35 ++++++++++++ pkg/middleware/middleware_test.go | 56 ++++++++++++------- pkg/middleware/session.go | 9 ++- .../sqlstore/migrations/migrations_test.go | 43 +++++++------- pkg/services/sqlstore/org_test.go | 1 - pkg/services/sqlstore/xorm.log | 0 pkg/setting/setting.go | 2 +- 7 files changed, 99 insertions(+), 47 deletions(-) create mode 100644 pkg/api/api_test.go delete mode 100644 pkg/services/sqlstore/xorm.log diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go new file mode 100644 index 00000000000..ea3588b0e67 --- /dev/null +++ b/pkg/api/api_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "testing" +) + +func TestHttpApi(t *testing.T) { + + // Convey("Given the grafana api", t, func() { + // ConveyApiScenario("Can sign up", func(c apiTestContext) { + // c.PostJson() + // So(c.Resp, ShouldEqualJsonApiResponse, "User created and logged in") + // }) + // + // m := macaron.New() + // m.Use(middleware.GetContextHandler()) + // m.Use(middleware.Sessioner(&session.Options{})) + // Register(m) + // + // var context *middleware.Context + // m.Get("/", func(c *middleware.Context) { + // context = c + // }) + // + // resp := httptest.NewRecorder() + // req, err := http.NewRequest("GET", "/", nil) + // So(err, ShouldBeNil) + // + // m.ServeHTTP(resp, req) + // + // Convey("should red 200", func() { + // So(resp.Code, ShouldEqual, 200) + // }) + // }) +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 9be76121718..9bfa73d2df7 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -10,31 +10,49 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +type scenarioContext struct { + m *macaron.Macaron + context *Context + resp *httptest.ResponseRecorder +} + +func (sc *scenarioContext) PerformGet(url string) { + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + sc.m.ServeHTTP(sc.resp, req) +} + +type scenarioFunc func(c *scenarioContext) + +func middlewareScenario(desc string, fn scenarioFunc) { + sc := &scenarioContext{} + + sc.m = macaron.New() + sc.m.Use(GetContextHandler()) + // mock out gc goroutine + startSessionGC = func() {} + sc.m.Use(Sessioner(&session.Options{})) + + sc.m.Get("/", func(c *Context) { + sc.context = c + }) + + sc.resp = httptest.NewRecorder() + fn(sc) +} + func TestMiddlewareContext(t *testing.T) { Convey("Given grafana context", t, func() { - m := macaron.New() - m.Use(GetContextHandler()) - m.Use(Sessioner(&session.Options{})) - - var context *Context - - m.Get("/", func(c *Context) { - context = c + middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) { + sc.PerformGet("/") + So(sc.context, ShouldNotBeNil) }) - resp := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/", nil) - So(err, ShouldBeNil) - - m.ServeHTTP(resp, req) - - Convey("Should be able to get grafana context in handlers", func() { - So(context, ShouldNotBeNil) + middlewareScenario("Default middleware should allow get request", func(sc *scenarioContext) { + sc.PerformGet("/") + So(sc.resp.Code, ShouldEqual, 200) }) - Convey("should return 200", func() { - So(resp.Code, ShouldEqual, 200) - }) }) } diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index c0a12cab781..7b036b9790e 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -17,10 +17,13 @@ const ( var sessionManager *session.Manager var sessionOptions *session.Options +var startSessionGC func() -func startSessionGC() { - sessionManager.GC() - time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC) +func init() { + startSessionGC = func() { + sessionManager.GC() + time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC) + } } func prepareOptions(opt *session.Options) *session.Options { diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index d4ba97450f7..0278ea6632b 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -1,12 +1,9 @@ package migrations import ( - "fmt" - "strings" "testing" "github.com/go-xorm/xorm" - "github.com/grafana/grafana/pkg/log" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" @@ -16,7 +13,7 @@ import ( var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"} func TestMigrations(t *testing.T) { - log.NewLogger(0, "console", `{"level": 0}`) + //log.NewLogger(0, "console", `{"level": 0}`) testDBs := []sqlutil.TestDB{ sqlutil.TestDB_Sqlite3, @@ -31,30 +28,30 @@ func TestMigrations(t *testing.T) { sqlutil.CleanDB(x) mg := NewMigrator(x) - mg.LogLevel = log.DEBUG + //mg.LogLevel = log.DEBUG AddMigrations(mg) err = mg.Start() So(err, ShouldBeNil) - tables, err := x.DBMetas() - So(err, ShouldBeNil) - - fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables)) - - for _, table := range tables { - fmt.Printf("\nTable: %v \n", table.Name) - for _, column := range table.Columns() { - fmt.Printf("\t %v \n", column.String(x.Dialect())) - } - - if len(table.Indexes) > 0 { - fmt.Printf("\n\tIndexes:\n") - for _, index := range table.Indexes { - fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type]) - } - } - } + // tables, err := x.DBMetas() + // So(err, ShouldBeNil) + // + // fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables)) + // + // for _, table := range tables { + // fmt.Printf("\nTable: %v \n", table.Name) + // for _, column := range table.Columns() { + // fmt.Printf("\t %v \n", column.String(x.Dialect())) + // } + // + // if len(table.Indexes) > 0 { + // fmt.Printf("\n\tIndexes:\n") + // for _, index := range table.Indexes { + // fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type]) + // } + // } + // } }) } } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 7d98da076e0..1b230e349ff 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -10,7 +10,6 @@ import ( ) func TestAccountDataAccess(t *testing.T) { - Convey("Testing Account DB Access", t, func() { InitTestDB(t) diff --git a/pkg/services/sqlstore/xorm.log b/pkg/services/sqlstore/xorm.log deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b451aad634f..f3c738af780 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -122,7 +122,7 @@ type CommandLineArgs struct { func init() { IsWindows = runtime.GOOS == "windows" - log.NewLogger(0, "console", `{"level": 0}`) + //log.NewLogger(0, "console", `{"level": 0}`) } func parseAppUrlAndSubUrl(section *ini.Section) (string, string) { From f416e2d1ac9812e8236f13ec831fe6c01783074d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 May 2015 22:26:16 +0200 Subject: [PATCH 109/398] More middleware unit test, starting to look really good --- pkg/middleware/middleware_test.go | 92 ++++++++++++++++++++++++++----- pkg/setting/setting.go | 2 +- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 9bfa73d2df7..e8e75465c82 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -1,44 +1,68 @@ package middleware import ( + "encoding/json" "net/http" "net/http/httptest" + "path/filepath" "testing" "github.com/Unknwon/macaron" + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" "github.com/macaron-contrib/session" . "github.com/smartystreets/goconvey/convey" ) type scenarioContext struct { - m *macaron.Macaron - context *Context - resp *httptest.ResponseRecorder + m *macaron.Macaron + context *Context + resp *httptest.ResponseRecorder + apiKey string + respJson map[string]interface{} } func (sc *scenarioContext) PerformGet(url string) { req, err := http.NewRequest("GET", "/", nil) So(err, ShouldBeNil) + if sc.apiKey != "" { + req.Header.Add("Authorization", "Bearer "+sc.apiKey) + } sc.m.ServeHTTP(sc.resp, req) + + if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { + err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) + So(err, ShouldBeNil) + } } type scenarioFunc func(c *scenarioContext) +type reqModifier func(c *http.Request) func middlewareScenario(desc string, fn scenarioFunc) { - sc := &scenarioContext{} + Convey(desc, func() { + sc := &scenarioContext{} + viewsPath, _ := filepath.Abs("../../public/views") - sc.m = macaron.New() - sc.m.Use(GetContextHandler()) - // mock out gc goroutine - startSessionGC = func() {} - sc.m.Use(Sessioner(&session.Options{})) + sc.m = macaron.New() + sc.m.Use(macaron.Renderer(macaron.RenderOptions{ + Directory: viewsPath, + Delims: macaron.Delims{Left: "[[", Right: "]]"}, + })) - sc.m.Get("/", func(c *Context) { - sc.context = c + sc.m.Use(GetContextHandler()) + // mock out gc goroutine + startSessionGC = func() {} + sc.m.Use(Sessioner(&session.Options{})) + + sc.m.Get("/", func(c *Context) { + sc.context = c + }) + + sc.resp = httptest.NewRecorder() + fn(sc) }) - - sc.resp = httptest.NewRecorder() - fn(sc) } func TestMiddlewareContext(t *testing.T) { @@ -54,5 +78,45 @@ func TestMiddlewareContext(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + middlewareScenario("Non api request should init session", func(sc *scenarioContext) { + sc.PerformGet("/") + So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess") + }) + + middlewareScenario("Invalid api key", func(sc *scenarioContext) { + sc.apiKey = "invalid_key_test" + sc.PerformGet("/") + + Convey("Should not init session", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty) + }) + + Convey("Should return 401", func() { + So(sc.resp.Code, ShouldEqual, 401) + So(sc.respJson["message"], ShouldEqual, "Invalid API key") + }) + }) + + middlewareScenario("Valid api key", func(sc *scenarioContext) { + sc.apiKey = "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9" + keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + + bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { + query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} + return nil + }) + + sc.PerformGet("/") + + Convey("Should return 200", func() { + So(sc.resp.Code, ShouldEqual, 200) + }) + + Convey("Should init middleware context", func() { + So(sc.context.OrgId, ShouldEqual, 12) + So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) + }) + }) + }) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f3c738af780..b451aad634f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -122,7 +122,7 @@ type CommandLineArgs struct { func init() { IsWindows = runtime.GOOS == "windows" - //log.NewLogger(0, "console", `{"level": 0}`) + log.NewLogger(0, "console", `{"level": 0}`) } func parseAppUrlAndSubUrl(section *ini.Section) (string, string) { From ba883d25fe1f19c115cc3c3e4b6df33ca0f0a9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 09:24:56 +0200 Subject: [PATCH 110/398] More middleware unit tests cover all current auth mechanisms --- pkg/bus/bus.go | 4 + pkg/middleware/middleware.go | 3 + pkg/middleware/middleware_test.go | 251 +++++++++++++++++++++--------- 3 files changed, 181 insertions(+), 77 deletions(-) diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 2865c740fd1..80b80698a1c 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -121,3 +121,7 @@ func Dispatch(msg Msg) error { func Publish(msg Msg) error { return globalBus.Publish(msg) } + +func ClearBusHandlers() { + globalBus = New() +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index b93cd517364..efeee0ac3e7 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,6 +1,7 @@ package middleware import ( + "fmt" "strconv" "strings" @@ -78,11 +79,13 @@ func initContextWithUserSessionCookie(ctx *Context) bool { var userId int64 if userId = getRequestUserId(ctx); userId == 0 { + fmt.Printf("Not userId") return false } query := m.GetSignedInUserQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { + log.Error(3, "Failed to get user with id %v", userId) return false } else { ctx.SignedInUser = query.Result diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index e8e75465c82..ebbf0a1f038 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -10,38 +10,131 @@ import ( "github.com/Unknwon/macaron" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/macaron-contrib/session" . "github.com/smartystreets/goconvey/convey" ) -type scenarioContext struct { - m *macaron.Macaron - context *Context - resp *httptest.ResponseRecorder - apiKey string - respJson map[string]interface{} +func TestMiddlewareContext(t *testing.T) { + + Convey("Given the grafana middleware", t, func() { + middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + So(sc.context, ShouldNotBeNil) + }) + + middlewareScenario("Default middleware should allow get request", func(sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + + middlewareScenario("Non api request should init session", func(sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess") + }) + + middlewareScenario("Invalid api key", func(sc *scenarioContext) { + sc.apiKey = "invalid_key_test" + sc.fakeReq("GET", "/").exec() + + Convey("Should not init session", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty) + }) + + Convey("Should return 401", func() { + So(sc.resp.Code, ShouldEqual, 401) + So(sc.respJson["message"], ShouldEqual, "Invalid API key") + }) + }) + + middlewareScenario("Valid api key", func(sc *scenarioContext) { + keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + + bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { + query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} + return nil + }) + + sc.fakeReq("GET", "/").withValidApiKey().exec() + + Convey("Should return 200", func() { + So(sc.resp.Code, ShouldEqual, 200) + }) + + Convey("Should init middleware context", func() { + So(sc.context.IsSignedIn, ShouldEqual, true) + So(sc.context.OrgId, ShouldEqual, 12) + So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) + }) + }) + + middlewareScenario("Valid api key, but does not match db hash", func(sc *scenarioContext) { + keyhash := "something_not_matching" + + bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { + query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} + return nil + }) + + sc.fakeReq("GET", "/").withValidApiKey().exec() + + Convey("Should return api key invalid", func() { + So(sc.resp.Code, ShouldEqual, 401) + So(sc.respJson["message"], ShouldEqual, "Invalid API key") + }) + }) + + middlewareScenario("UserId in session", func(sc *scenarioContext) { + + sc.fakeReq("GET", "/").handler(func(c *Context) { + c.Session.Set(SESS_KEY_USERID, int64(12)) + }).exec() + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.fakeReq("GET", "/").exec() + + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + }) + }) + + middlewareScenario("When anonymous access is enabled", func(sc *scenarioContext) { + setting.AnonymousEnabled = true + setting.AnonymousOrgName = "test" + setting.AnonymousOrgRole = string(m.ROLE_EDITOR) + + bus.AddHandler("test", func(query *m.GetOrgByNameQuery) error { + So(query.Name, ShouldEqual, "test") + + query.Result = &m.Org{Id: 2, Name: "test"} + return nil + }) + + sc.fakeReq("GET", "/").exec() + + Convey("should init context with org info", func() { + So(sc.context.UserId, ShouldEqual, 0) + So(sc.context.OrgId, ShouldEqual, 2) + So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) + }) + + Convey("context signed in should be false", func() { + So(sc.context.IsSignedIn, ShouldBeFalse) + }) + }) + }) } -func (sc *scenarioContext) PerformGet(url string) { - req, err := http.NewRequest("GET", "/", nil) - So(err, ShouldBeNil) - if sc.apiKey != "" { - req.Header.Add("Authorization", "Bearer "+sc.apiKey) - } - sc.m.ServeHTTP(sc.resp, req) - - if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { - err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) - So(err, ShouldBeNil) - } -} - -type scenarioFunc func(c *scenarioContext) -type reqModifier func(c *http.Request) - func middlewareScenario(desc string, fn scenarioFunc) { Convey(desc, func() { + defer bus.ClearBusHandlers() + sc := &scenarioContext{} viewsPath, _ := filepath.Abs("../../public/views") @@ -58,65 +151,69 @@ func middlewareScenario(desc string, fn scenarioFunc) { sc.m.Get("/", func(c *Context) { sc.context = c + if sc.handlerFunc != nil { + sc.handlerFunc(sc.context) + } }) - sc.resp = httptest.NewRecorder() fn(sc) }) } -func TestMiddlewareContext(t *testing.T) { +type scenarioContext struct { + m *macaron.Macaron + context *Context + resp *httptest.ResponseRecorder + apiKey string + respJson map[string]interface{} + handlerFunc handlerFunc - Convey("Given grafana context", t, func() { - middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) { - sc.PerformGet("/") - So(sc.context, ShouldNotBeNil) - }) - - middlewareScenario("Default middleware should allow get request", func(sc *scenarioContext) { - sc.PerformGet("/") - So(sc.resp.Code, ShouldEqual, 200) - }) - - middlewareScenario("Non api request should init session", func(sc *scenarioContext) { - sc.PerformGet("/") - So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess") - }) - - middlewareScenario("Invalid api key", func(sc *scenarioContext) { - sc.apiKey = "invalid_key_test" - sc.PerformGet("/") - - Convey("Should not init session", func() { - So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty) - }) - - Convey("Should return 401", func() { - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, "Invalid API key") - }) - }) - - middlewareScenario("Valid api key", func(sc *scenarioContext) { - sc.apiKey = "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9" - keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") - - bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { - query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} - return nil - }) - - sc.PerformGet("/") - - Convey("Should return 200", func() { - So(sc.resp.Code, ShouldEqual, 200) - }) - - Convey("Should init middleware context", func() { - So(sc.context.OrgId, ShouldEqual, 12) - So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) - }) - }) - - }) + req *http.Request } + +func (sc *scenarioContext) withValidApiKey() *scenarioContext { + sc.apiKey = "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9" + return sc +} + +func (sc *scenarioContext) withInvalidApiKey() *scenarioContext { + sc.apiKey = "nvalidhhhhds" + return sc +} + +func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + So(err, ShouldBeNil) + sc.req = req + + // add session cookie from last request + if sc.context != nil { + if sc.context.Session.ID() != "" { + req.Header.Add("Cookie", "grafana_sess="+sc.context.Session.ID()+";") + } + } + + return sc +} + +func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext { + sc.handlerFunc = fn + return sc +} + +func (sc *scenarioContext) exec() { + if sc.apiKey != "" { + sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey) + } + + sc.m.ServeHTTP(sc.resp, sc.req) + + if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { + err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) + So(err, ShouldBeNil) + } +} + +type scenarioFunc func(c *scenarioContext) +type handlerFunc func(c *Context) From be589d81c71714a607c26bf33cd88578aafdc456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 12:06:58 +0200 Subject: [PATCH 111/398] Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER), Closes #1921 --- CHANGELOG.md | 2 +- pkg/middleware/auth_proxy.go | 67 +++++++++++++++++++++++++++ pkg/middleware/auth_test.go | 35 +++++++++++++++ pkg/middleware/middleware.go | 3 +- pkg/middleware/middleware_test.go | 75 +++++++++++++++++++++++++++---- pkg/models/user.go | 2 + pkg/services/sqlstore/user.go | 11 ++++- 7 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 pkg/middleware/auth_proxy.go create mode 100644 pkg/middleware/auth_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e4f5482b3..a8f21eae095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images - +- [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER) # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go new file mode 100644 index 00000000000..d735779f694 --- /dev/null +++ b/pkg/middleware/auth_proxy.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func initContextWithAuthProxy(ctx *Context) bool { + if !setting.AuthProxyEnabled { + return false + } + + proxyHeaderValue := ctx.Req.Header.Get(setting.AuthProxyHeaderName) + if len(proxyHeaderValue) <= 0 { + return false + } + + query := getSignedInUserQueryForProxyAuth(proxyHeaderValue) + if err := bus.Dispatch(query); err != nil { + if err != m.ErrUserNotFound { + ctx.Handle(500, "Failed find user specifed in auth proxy header", err) + return true + } + + if setting.AuthProxyAutoSignUp { + cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue) + if err := bus.Dispatch(cmd); err != nil { + ctx.Handle(500, "Failed to create user specified in auth proxy header", err) + return true + } + query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id} + if err := bus.Dispatch(query); err != nil { + ctx.Handle(500, "Failed find user after creation", err) + return true + } + } + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + return true +} + +func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery { + query := m.GetSignedInUserQuery{} + if setting.AuthProxyHeaderProperty == "username" { + query.Login = headerVal + } else if setting.AuthProxyHeaderProperty == "email" { + query.Email = headerVal + } else { + panic("Auth proxy header property invalid") + } + return &query +} + +func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand { + cmd := m.CreateUserCommand{} + if setting.AuthProxyHeaderProperty == "username" { + cmd.Login = headerVal + } else if setting.AuthProxyHeaderProperty == "email" { + cmd.Email = headerVal + } else { + panic("Auth proxy header property invalid") + } + return &cmd +} diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go new file mode 100644 index 00000000000..81b0f525e98 --- /dev/null +++ b/pkg/middleware/auth_test.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestMiddlewareAuth(t *testing.T) { + + Convey("Given the grafana middleware", t, func() { + reqSignIn := Auth(&AuthOptions{ReqSignedIn: true}) + + middlewareScenario("ReqSignIn true and unauthenticated request", func(sc *scenarioContext) { + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/secure").exec() + + Convey("Should redirect to login", func() { + So(sc.resp.Code, ShouldEqual, 302) + }) + }) + + middlewareScenario("ReqSignIn true and unauthenticated API request", func(sc *scenarioContext) { + sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/api/secure").exec() + + Convey("Should return 401", func() { + So(sc.resp.Code, ShouldEqual, 401) + }) + }) + + }) +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index efeee0ac3e7..2a6873076c6 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "strconv" "strings" @@ -41,6 +40,7 @@ func GetContextHandler() macaron.Handler { // then look for api key in session (special case for render calls via api) // then test if anonymous access is enabled if initContextWithApiKey(ctx) || + initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(ctx) || initContextWithApiKeyFromSession(ctx) || initContextWithAnonymousUser(ctx) { @@ -79,7 +79,6 @@ func initContextWithUserSessionCookie(ctx *Context) bool { var userId int64 if userId = getRequestUserId(ctx); userId == 0 { - fmt.Printf("Not userId") return false } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index ebbf0a1f038..212e250cc1c 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -128,6 +128,62 @@ func TestMiddlewareContext(t *testing.T) { So(sc.context.IsSignedIn, ShouldBeFalse) }) }) + + middlewareScenario("When auth_proxy is enabled enabled and user exists", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.exec() + + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.OrgId, ShouldEqual, 2) + }) + }) + + middlewareScenario("When auth_proxy is enabled enabled and user does not exists", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyAutoSignUp = true + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + if query.UserId > 0 { + query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} + return nil + } else { + return m.ErrUserNotFound + } + }) + + var createUserCmd *m.CreateUserCommand + bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { + createUserCmd = cmd + cmd.Result = m.User{Id: 33} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.exec() + + Convey("Should create user if auto sign up is enabled", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 33) + So(sc.context.OrgId, ShouldEqual, 4) + + }) + }) + }) } @@ -149,24 +205,27 @@ func middlewareScenario(desc string, fn scenarioFunc) { startSessionGC = func() {} sc.m.Use(Sessioner(&session.Options{})) - sc.m.Get("/", func(c *Context) { + sc.defaultHandler = func(c *Context) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) } - }) + } + + sc.m.Get("/", sc.defaultHandler) fn(sc) }) } type scenarioContext struct { - m *macaron.Macaron - context *Context - resp *httptest.ResponseRecorder - apiKey string - respJson map[string]interface{} - handlerFunc handlerFunc + m *macaron.Macaron + context *Context + resp *httptest.ResponseRecorder + apiKey string + respJson map[string]interface{} + handlerFunc handlerFunc + defaultHandler macaron.Handler req *http.Request } diff --git a/pkg/models/user.go b/pkg/models/user.go index cdc688d4c1f..68e0001b99c 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -89,6 +89,8 @@ type GetUserByIdQuery struct { type GetSignedInUserQuery struct { UserId int64 + Login string + Email string Result *SignedInUser } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 61798d724ef..c2092e963f8 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -263,8 +263,15 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { org.id as org_id FROM ` + dialect.Quote("user") + ` as u LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id - LEFT OUTER JOIN org on org.id = u.org_id - WHERE u.id=?` + LEFT OUTER JOIN org on org.id = u.org_id ` + + if query.UserId > 0 { + rawSql += "WHERE u.id=?" + } else if query.Login != "" { + rawSql += "WHERE u.login=?" + } else if query.Email != "" { + rawSql += "WHERE u.email=?" + } var user m.SignedInUser sess := x.Table("user") From 38fc85d6191b5cc948c26a8ae25829142bff52e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 12:30:53 +0200 Subject: [PATCH 112/398] Final tweaks to auth proxy feature --- conf/defaults.ini | 2 +- conf/sample.ini | 7 +++++++ pkg/middleware/auth_proxy.go | 4 +++- pkg/services/sqlstore/user.go | 10 +++++----- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 88218240bac..b0a085dd991 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -164,7 +164,7 @@ allowed_domains = #################################### Auth Proxy ########################## [auth.proxy] -enabled = false; +enabled = false header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true diff --git a/conf/sample.ini b/conf/sample.ini index aa333cdc5de..e86846da232 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -161,6 +161,13 @@ ;api_url = https://www.googleapis.com/oauth2/v1/userinfo ;allowed_domains = +#################################### Auth Proxy ########################## +[auth.proxy] +;enabled = false +;header_name = X-WEBAUTH-USER +;header_property = username +;auto_sign_up = true + #################################### Logging ########################## [log] # Either "console", "file", default is "console" diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index d735779f694..42cd91a3ad1 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -12,7 +12,7 @@ func initContextWithAuthProxy(ctx *Context) bool { } proxyHeaderValue := ctx.Req.Header.Get(setting.AuthProxyHeaderName) - if len(proxyHeaderValue) <= 0 { + if len(proxyHeaderValue) == 0 { return false } @@ -34,6 +34,8 @@ func initContextWithAuthProxy(ctx *Context) bool { ctx.Handle(500, "Failed find user after creation", err) return true } + } else { + return false } } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index c2092e963f8..663496427ba 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -265,17 +265,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id LEFT OUTER JOIN org on org.id = u.org_id ` + sess := x.Table("user") if query.UserId > 0 { - rawSql += "WHERE u.id=?" + sess.Sql(rawSql+"WHERE u.id=?", query.UserId) } else if query.Login != "" { - rawSql += "WHERE u.login=?" + sess.Sql(rawSql+"WHERE u.login=?", query.Login) } else if query.Email != "" { - rawSql += "WHERE u.email=?" + sess.Sql(rawSql+"WHERE u.email=?", query.Email) } var user m.SignedInUser - sess := x.Table("user") - has, err := sess.Sql(rawSql, query.UserId).Get(&user) + has, err := sess.Get(&user) if err != nil { return err } else if !has { From a4a8cd05d298b0f67ca93f2f9f0c7f8f10b1082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 16:59:19 +0200 Subject: [PATCH 113/398] Updated to nodejs packages and karma code coverage setup --- package.json | 21 +++++++++------------ tasks/options/karma.js | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 8785632eaee..5016a6309eb 100644 --- a/package.json +++ b/package.json @@ -34,22 +34,19 @@ "grunt-string-replace": "~0.2.4", "grunt-usemin": "3.0.0", "jshint-stylish": "~0.1.5", - "karma": "~0.12.21", + "karma": "~0.12.31", "karma-chrome-launcher": "~0.1.4", "karma-coffee-preprocessor": "~0.1.2", - "karma-coverage": "^0.2.5", - "karma-coveralls": "^0.1.4", + "karma-coverage": "0.3.1", + "karma-coveralls": "0.1.5", "karma-expect": "~1.1.0", - "karma-firefox-launcher": "~0.1.3", - "karma-html2js-preprocessor": "~0.1.0", - "karma-jasmine": "~0.2.2", "karma-mocha": "~0.1.4", - "karma-phantomjs-launcher": "~0.1.4", - "karma-requirejs": "~0.2.1", - "karma-script-launcher": "~0.1.0", - "load-grunt-tasks": "~0.2.0", - "mocha": "~1.16.1", - "requirejs": "~2.1.14", + "karma-phantomjs-launcher": "0.1.4", + "karma-requirejs": "0.2.2", + "karma-script-launcher": "0.1.0", + "load-grunt-tasks": "0.2.0", + "mocha": "2.2.4", + "requirejs": "2.1.17", "rjs-build-analysis": "0.0.3" }, "engines": { diff --git a/tasks/options/karma.js b/tasks/options/karma.js index 630dc19878e..44f11d62a23 100644 --- a/tasks/options/karma.js +++ b/tasks/options/karma.js @@ -16,7 +16,7 @@ module.exports = function(config) { configFile: '<%= srcDir %>/test/karma.conf.js', reporters: ['dots','coverage','coveralls'], preprocessors: { - '<%= srcDir %>/app/**/*.js': ['coverage'] + 'public/app/**/*.js': ['coverage'] }, coverageReporter: { type: 'lcov', From b51a8f11d506f276baf56797b8f2fe66415ef2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 17:05:07 +0200 Subject: [PATCH 114/398] Added back coveralls coverage --- circle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/circle.yml b/circle.yml index c2ea5920bb6..32ace393c8b 100644 --- a/circle.yml +++ b/circle.yml @@ -22,3 +22,4 @@ test: - godep go test -v ./pkg/... # js tests - ./node_modules/grunt-cli/bin/grunt test + - npm run coveralls From 697529d0e832fcfaa058698e87374f41dbec381c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 May 2015 17:24:03 +0200 Subject: [PATCH 115/398] Worked on golang code coverage via coveralls --- .gitignore | 1 + test.sh | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100755 test.sh diff --git a/.gitignore b/.gitignore index c3eb5d8862c..99ed4c4adfd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ public/css/*.min.css conf/custom.ini fig.yml +profile.cov diff --git a/test.sh b/test.sh new file mode 100755 index 00000000000..881497029f9 --- /dev/null +++ b/test.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# The script does automatic checking on a Go package and its sub-packages, including: +# 1. gofmt (http://golang.org/cmd/gofmt/) +# 2. goimports (https://github.com/bradfitz/goimports) +# 3. golint (https://github.com/golang/lint) +# 4. go vet (http://golang.org/cmd/vet) +# 5. race detector (http://blog.golang.org/race-detector) +# 6. test coverage (http://blog.golang.org/cover) + +set -e + +# Automatic checks +# test -z "$(gofmt -s -l . | grep -v Godeps/_workspace/src/ | tee /dev/stderr)" +# go vet ./pkg/... +# godep go test -v -race ./pkg/... + +echo "mode: count" > profile.cov + +# Standard go tooling behavior is to ignore dirs with leading underscors +for dir in $(find ./pkg/ -maxdepth 4 -type d); +do +if ls $dir/*.go &> /dev/null; then + godep go test -covermode=count -coverprofile=$dir/profile.tmp $dir + if [ -f $dir/profile.tmp ] + then + cat $dir/profile.tmp | tail -n +2 >> profile.cov + rm $dir/profile.tmp + fi +fi +done + +go tool cover -func profile.cov + +# To submit the test coverage result to coveralls.io, +# use goveralls (https://github.com/mattn/goveralls) +# goveralls -coverprofile=profile.cov -service=travis-ci From 020d7d4939695a80b6c1bae6d0e2bdce9e4528d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 May 2015 16:46:46 +0200 Subject: [PATCH 116/398] Update docs, fixed mistake in http api docs, Fixes #1928 --- docs/sources/reference/http_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index f02741cb37b..c0b63050199 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -121,7 +121,7 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver "isStarred": false, "slug": "production-overview" }, - "dashboard": { + "model": { "id": null, "title": "Production Overview", "tags": [ "templated" ], From 73ee8a59857706d027a3285f304d82079d0e89da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2015 07:46:46 +0200 Subject: [PATCH 117/398] HTTP API: fix for POST /api/dashboards/db returned 200 ok when dashboard was not found, Fixes #1929 --- pkg/api/dashboard.go | 4 ++++ pkg/models/dashboards.go | 2 +- pkg/services/sqlstore/dashboard.go | 10 ++++++++-- pkg/services/sqlstore/dashboard_test.go | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 278264f22d7..9ea5089ee7d 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -87,6 +87,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()}) return } + if err == m.ErrDashboardNotFound { + c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()}) + return + } c.JsonApiErr(500, "Failed to save dashboard", err) return } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 159d4e1a3c6..234ae74f04a 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -10,7 +10,7 @@ import ( // Typed errors var ( - ErrDashboardNotFound = errors.New("Account not found") + ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 7dbebd94e4c..f4be6d55edf 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -48,13 +48,19 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } + affectedRows := int64(0) + if dash.Id == 0 { metrics.M_Models_Dashboard_Insert.Inc(1) - _, err = sess.Insert(dash) + affectedRows, err = sess.Insert(dash) } else { dash.Version += 1 dash.Data["version"] = dash.Version - _, err = sess.Id(dash.Id).Update(dash) + affectedRows, err = sess.Id(dash.Id).Update(dash) + } + + if affectedRows == 0 { + return m.ErrDashboardNotFound } // delete existing tabs diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index c7a8053d528..4c6614ed774 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -51,6 +51,21 @@ func TestDashboardDataAccess(t *testing.T) { So(query.Result.Slug, ShouldEqual, "test-dash-23") }) + Convey("Should return error if no dashboard is updated", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Overwrite: true, + Dashboard: map[string]interface{}{ + "id": float64(123412321), + "title": "Expect error", + "tags": []interface{}{}, + }, + } + + err := SaveDashboard(&cmd) + So(err, ShouldNotBeNil) + }) + Convey("Should be able to search for dashboard", func() { query := m.SearchDashboardsQuery{ Title: "test", From 38d851eb9859d556f99cfb65ef3606b819d579d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2015 08:19:29 +0200 Subject: [PATCH 118/398] Another HTTP API fix --- pkg/services/sqlstore/dashboard.go | 38 ++++++++++++++++--------- pkg/services/sqlstore/dashboard_test.go | 18 ++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index f4be6d55edf..d34a6fcdd51 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -23,21 +23,17 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { dash := cmd.GetDashboardModel() // try get existing dashboard - existing := m.Dashboard{Slug: dash.Slug, OrgId: dash.OrgId} - hasExisting, err := sess.Get(&existing) - if err != nil { - return err - } + var existing, sameTitle m.Dashboard - if hasExisting { - // another dashboard with same name - if dash.Id != existing.Id { - if cmd.Overwrite { - dash.Id = existing.Id - } else { - return m.ErrDashboardWithSameNameExists - } + if dash.Id > 0 { + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + if err != nil { + return err } + if !dashWithIdExists { + return m.ErrDashboardNotFound + } + // check for is someone else has written in between if dash.Version != existing.Version { if cmd.Overwrite { @@ -48,6 +44,22 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } + sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) + if err != nil { + return err + } + + if sameTitleExists { + // another dashboard with same name + if dash.Id != sameTitle.Id { + if cmd.Overwrite { + dash.Id = sameTitle.Id + } else { + return m.ErrDashboardWithSameNameExists + } + } + } + affectedRows := int64(0) if dash.Id == 0 { diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 4c6614ed774..ad4974af18b 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -66,6 +66,24 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("Should not be able to overwrite dashboard in another org", func() { + query := m.GetDashboardQuery{Slug: "test-dash-23", OrgId: 1} + GetDashboard(&query) + + cmd := m.SaveDashboardCommand{ + OrgId: 2, + Overwrite: true, + Dashboard: map[string]interface{}{ + "id": float64(query.Result.Id), + "title": "Expect error", + "tags": []interface{}{}, + }, + } + + err := SaveDashboard(&cmd) + So(err, ShouldNotBeNil) + }) + Convey("Should be able to search for dashboard", func() { query := m.SearchDashboardsQuery{ Title: "test", From e5c11691203fe68958e66693e429f6f5a3c77200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2015 08:36:44 +0200 Subject: [PATCH 119/398] HTTP API: GET /api/dashboards/db/:slug response changed property to to match the POST request nameing, Fixes #1928 --- CHANGELOG.md | 3 +++ pkg/api/dashboard.go | 10 +++++----- pkg/api/dashboard_snapshot.go | 6 +++--- pkg/api/dtos/models.go | 6 +++--- public/app/features/dashboard/dashboardCtrl.js | 2 +- public/app/features/panel/soloPanelCtrl.js | 2 +- public/app/routes/dashLoadControllers.js | 12 ++++++------ public/test/specs/soloPanelCtrl-specs.js | 2 +- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f21eae095..148d4290907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images - [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER) +**Breaking changes** +- [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing + # 2.0.3 (unreleased - 2.0.x branch) **Fixes** diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 9ea5089ee7d..99c408c4e15 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -46,9 +46,9 @@ func GetDashboard(c *middleware.Context) { } dash := query.Result - dto := dtos.Dashboard{ - Model: dash.Data, - Meta: dtos.DashboardMeta{IsStarred: isStarred, Slug: slug}, + dto := dtos.DashboardFullWithMeta{ + Dashboard: dash.Data, + Meta: dtos.DashboardMeta{IsStarred: isStarred, Slug: slug}, } c.JSON(200, dto) @@ -108,10 +108,10 @@ func GetHomeDashboard(c *middleware.Context) { return } - dash := dtos.Dashboard{} + dash := dtos.DashboardFullWithMeta{} dash.Meta.IsHome = true jsonParser := json.NewDecoder(file) - if err := jsonParser.Decode(&dash.Model); err != nil { + if err := jsonParser.Decode(&dash.Dashboard); err != nil { c.JsonApiErr(500, "Failed to load home dashboard", err) return } diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index fe628d580b1..7bdb6807890 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -45,8 +45,8 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho } func GetDashboardSnapshot(c *middleware.Context) { - key := c.Params(":key") + key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{Key: key} err := bus.Dispatch(query) @@ -63,8 +63,8 @@ func GetDashboardSnapshot(c *middleware.Context) { return } - dto := dtos.Dashboard{ - Model: snapshot.Dashboard, + dto := dtos.DashboardFullWithMeta{ + Dashboard: snapshot.Dashboard, Meta: dtos.DashboardMeta{ IsSnapshot: true, Created: snapshot.Created, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index fea88f07550..ce0436eccfa 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -37,9 +37,9 @@ type DashboardMeta struct { Created time.Time `json:"created"` } -type Dashboard struct { - Meta DashboardMeta `json:"meta"` - Model map[string]interface{} `json:"model"` +type DashboardFullWithMeta struct { + Meta DashboardMeta `json:"meta"` + Dashboard map[string]interface{} `json:"dashboard"` } type DataSource struct { diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index f81b808a0e1..d8772fa3988 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -40,7 +40,7 @@ function (angular, $, config) { $rootScope.performance.panelsInitialized = 0; $rootScope.performance.panelsRendered = 0; - var dashboard = dashboardSrv.create(data.model, data.meta); + var dashboard = dashboardSrv.create(data.dashboard, data.meta); // init services timeSrv.init(dashboard); diff --git a/public/app/features/panel/soloPanelCtrl.js b/public/app/features/panel/soloPanelCtrl.js index f593f8ee950..977f634a905 100644 --- a/public/app/features/panel/soloPanelCtrl.js +++ b/public/app/features/panel/soloPanelCtrl.js @@ -41,7 +41,7 @@ function (angular, $) { }; $scope.initPanelScope = function(dashData) { - $scope.dashboard = dashboardSrv.create(dashData.model, dashData.meta); + $scope.dashboard = dashboardSrv.create(dashData.dashboard, dashData.meta); $scope.row = { height: ($(window).height() - 10) + 'px', diff --git a/public/app/routes/dashLoadControllers.js b/public/app/routes/dashLoadControllers.js index f85cee09d84..d26827248ce 100644 --- a/public/app/routes/dashLoadControllers.js +++ b/public/app/routes/dashLoadControllers.js @@ -13,7 +13,7 @@ function (angular, _, kbn, moment, $) { module.controller('DashFromDBCtrl', function($scope, $routeParams, backendSrv) { function dashboardLoadFailed(title) { - $scope.initDashboard({meta: {}, model: {title: title}}, $scope); + $scope.initDashboard({meta: {}, dashboard: {title: title}}, $scope); } if (!$routeParams.slug) { @@ -46,7 +46,7 @@ function (angular, _, kbn, moment, $) { canSave: false, canEdit: false, }, - model: { + dashboard: { title: 'Snapshot not found' } }, $scope); @@ -61,14 +61,14 @@ function (angular, _, kbn, moment, $) { } $scope.initDashboard({ meta: { canShare: false, canStar: false }, - model: window.grafanaImportDashboard + dashboard: window.grafanaImportDashboard }, $scope); }); module.controller('NewDashboardCtrl', function($scope) { $scope.initDashboard({ meta: { canStar: false, canShare: false }, - model: { + dashboard: { title: "New dashboard", rows: [{ height: '250px', panels:[] }] }, @@ -98,7 +98,7 @@ function (angular, _, kbn, moment, $) { file_load($routeParams.jsonFile).then(function(result) { $scope.initDashboard({ meta: { canSave: false, canDelete: false }, - model: result + dashboard: result }, $scope); }); @@ -146,7 +146,7 @@ function (angular, _, kbn, moment, $) { script_load($routeParams.jsFile).then(function(result) { $scope.initDashboard({ meta: {fromScript: true, canDelete: false, canSave: false}, - model: result.data + dashboard: result.data }, $scope); }); diff --git a/public/test/specs/soloPanelCtrl-specs.js b/public/test/specs/soloPanelCtrl-specs.js index 44c9f2080cc..09271c1be03 100644 --- a/public/test/specs/soloPanelCtrl-specs.js +++ b/public/test/specs/soloPanelCtrl-specs.js @@ -34,7 +34,7 @@ define([ beforeEach(function() { var dashboard = { - model: { + dashboard: { rows: [ { panels: [ From 385048b6200f3069b9d158958f70af4d64674f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2015 10:13:12 +0200 Subject: [PATCH 120/398] Templating: You can now select multiple template variables values at the same time. Closes #1922 --- CHANGELOG.md | 1 + .../features/templating/templateValuesSrv.js | 4 ++ public/test/specs/helpers.js | 1 + public/test/specs/templateValuesSrv-specs.js | 50 ++++++++++++++++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 148d4290907..523033ef243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ **New dashboard features** - [Issue #1144](https://github.com/grafana/grafana/issues/1144). Templating: You can now select multiple template variables values at the same time. +- [Issue #1922](https://github.com/grafana/grafana/issues/1922). Templating: Specify multiple variable values via URL params. - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value **User or Organization admin** diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 9dea95fb083..6db5ce595a2 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -57,6 +57,10 @@ function (angular, _, kbn) { var option = _.findWhere(variable.options, { text: urlValue }); option = option || { text: urlValue, value: urlValue }; + if (_.isArray(urlValue)) { + option.text = urlValue.join(', '); + } + this.updateAutoInterval(variable); return this.setVariableValue(variable, option); }; diff --git a/public/test/specs/helpers.js b/public/test/specs/helpers.js index a9d8a09bfe4..0a0c7b77952 100644 --- a/public/test/specs/helpers.js +++ b/public/test/specs/helpers.js @@ -69,6 +69,7 @@ define([ self.timeSrv = new TimeSrvStub(); self.datasourceSrv = {}; self.backendSrv = {}; + self.$location = {}; self.$routeParams = {}; this.providePhase = function(mocks) { diff --git a/public/test/specs/templateValuesSrv-specs.js b/public/test/specs/templateValuesSrv-specs.js index 323b1495404..2ff287b8b6d 100644 --- a/public/test/specs/templateValuesSrv-specs.js +++ b/public/test/specs/templateValuesSrv-specs.js @@ -10,7 +10,7 @@ define([ var ctx = new helpers.ServiceTestContext(); beforeEach(module('grafana.services')); - beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', "$routeParams"])); + beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); beforeEach(ctx.createService('templateValuesSrv')); describe('update interval variable options', function() { @@ -27,11 +27,57 @@ define([ }); }); + describe('when template variable is present in url', function() { + var variable = { + name: 'apps', + current: {text: "test", value: "test"}, + options: [{text: "test", value: "test"}] + }; + + beforeEach(function() { + var dashboard = { templating: { list: [variable] } }; + var urlParams = {}; + urlParams["var-apps"] = "new"; + ctx.$location.search = sinon.stub().returns(urlParams); + ctx.service.init(dashboard); + }); + + it('should update current value', function() { + expect(variable.current.value).to.be("new"); + expect(variable.current.text).to.be("new"); + }); + }); + + describe('when template variable is present in url multiple times', function() { + var variable = { + name: 'apps', + multi: true, + current: {text: "test", value: "test"}, + options: [{text: "test", value: "test"}] + }; + + beforeEach(function() { + var dashboard = { templating: { list: [variable] } }; + var urlParams = {}; + urlParams["var-apps"] = ["new", "other"]; + ctx.$location.search = sinon.stub().returns(urlParams); + ctx.service.init(dashboard); + }); + + it('should update current value', function() { + expect(variable.current.value.length).to.be(2); + expect(variable.current.value[0]).to.be("new"); + expect(variable.current.value[1]).to.be("other"); + expect(variable.current.text).to.be("new, other"); + }); + }); + + function describeUpdateVariable(desc, fn) { describe(desc, function() { var scenario = {}; scenario.setup = function(setupFn) { - scenario.setupFn = setupFn; + scenario.setupFn = setupFn; }; beforeEach(function() { From c628ac9bbbf6943756f383ea9eea47ac7c13d44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2015 14:36:46 +0200 Subject: [PATCH 121/398] Upgraded to JQuery 2.1.1 -> 2.1.3, and spectrum.js, Fixes #1932 --- public/app/components/require.config.js | 2 +- public/vendor/jquery/jquery-2.1.1.min.js | 4 - public/vendor/jquery/jquery-2.1.3.js | 9205 ++++++++++++++++++++++ public/vendor/spectrum.js | 841 +- 4 files changed, 9769 insertions(+), 283 deletions(-) delete mode 100644 public/vendor/jquery/jquery-2.1.1.min.js create mode 100644 public/vendor/jquery/jquery-2.1.3.js diff --git a/public/app/components/require.config.js b/public/app/components/require.config.js index c613d1cc705..bbe9674aecf 100644 --- a/public/app/components/require.config.js +++ b/public/app/components/require.config.js @@ -29,7 +29,7 @@ require.config({ 'lodash-src': '../vendor/lodash', bootstrap: '../vendor/bootstrap/bootstrap', - jquery: '../vendor/jquery/jquery-2.1.1.min', + jquery: '../vendor/jquery/jquery-2.1.3', 'extend-jquery': 'components/extend-jquery', diff --git a/public/vendor/jquery/jquery-2.1.1.min.js b/public/vendor/jquery/jquery-2.1.1.min.js deleted file mode 100644 index e5ace116b6f..00000000000 --- a/public/vendor/jquery/jquery-2.1.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
    ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="
    ","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) -},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("'; - $scope.imageUrl = soloUrl.replace('/dashboard/', '/render/dashboard/'); + $scope.imageUrl = soloUrl.replace('/dashboard', '/render/dashboard'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; }; diff --git a/public/app/features/panel/soloPanelCtrl.js b/public/app/features/panel/soloPanelCtrl.js index 3d9ea3f499a..55e1c029200 100644 --- a/public/app/features/panel/soloPanelCtrl.js +++ b/public/app/features/panel/soloPanelCtrl.js @@ -7,16 +7,7 @@ function (angular, $) { var module = angular.module('grafana.routes'); - module.controller('SoloPanelCtrl', function( - $scope, - backendSrv, - $routeParams, - dashboardSrv, - timeSrv, - $location, - templateValuesSrv, - dashboardLoaderSrv, - contextSrv) { + module.controller('SoloPanelCtrl', function($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) { var panelId; @@ -28,7 +19,6 @@ function (angular, $) { dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { $scope.initDashboard(result, $scope); - }); $scope.onAppEvent("dashboard-loaded", $scope.initPanelScope); @@ -49,10 +39,7 @@ function (angular, $) { } $scope.panel.span = 12; - $scope.dashboardViewState = { registerPanel: function() { }, state: {}}; - - timeSrv.init($scope.dashboard); - templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState); + $scope.dashboardViewState = {registerPanel: function() { }, state: {}}; }; if (!$scope.skipAutoInit) { diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 1a28ddc74a2..85383042a21 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -67,7 +67,7 @@ New - + Import diff --git a/public/test/specs/soloPanelCtrl-specs.js b/public/test/specs/soloPanelCtrl-specs.js deleted file mode 100644 index 09271c1be03..00000000000 --- a/public/test/specs/soloPanelCtrl-specs.js +++ /dev/null @@ -1,73 +0,0 @@ -define([ - 'helpers', - 'features/panel/soloPanelCtrl', - 'features/dashboard/dashboardSrv', -], function(helpers) { - 'use strict'; - - describe('SoloPanelCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - var backendSrv = {}; - var routeParams = {}; - var search = {}; - var contextSrv = { - hasRole: sinon.stub().returns(true) - }; - - beforeEach(module('grafana.routes')); - beforeEach(module('grafana.services')); - beforeEach(ctx.providePhase({ - $routeParams: routeParams, - contextSrv: contextSrv, - $location: { - search: function() { - return search; - } - }, - templateValuesSrv: { init: sinon.stub() }, - backendSrv: backendSrv - })); - - beforeEach(ctx.createControllerPhase('SoloPanelCtrl')); - - describe('setting up solo panel scope', function() { - - beforeEach(function() { - var dashboard = { - dashboard: { - rows: [ - { - panels: [ - { - id: 23, - some: 'prop' - } - ] - } - ] - }, - meta: {} - }; - - routeParams.slug = "my dash"; - search.panelId = 23; - backendSrv.getDashboard = sinon.stub().returns(ctx.$q.when(dashboard)); - - ctx.scope.init(); - ctx.scope.$digest(); - }); - - it('should load dashboard and extract panel and setup panel scope', function() { - expect(ctx.scope.panel.id).to.be(23); - expect(ctx.scope.panel.some).to.be('prop'); - }); - - it('should hide sidemenu', function() { - expect(contextSrv.sidemenu).to.be(false); - }); - - }); - - }); - -}); diff --git a/public/test/test-main.js b/public/test/test-main.js index c49c47429f9..3e944c3c36f 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -139,7 +139,6 @@ require([ 'specs/kbn-format-specs', 'specs/dashboardSrv-specs', 'specs/dashboardViewStateSrv-specs', - 'specs/soloPanelCtrl-specs', 'specs/singlestat-specs', 'specs/dynamicDashboardSrv-specs', 'specs/unsavedChangesSrv-specs', From 187834b17c938b911d5e825133284fc6ab93d836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 09:58:45 +0200 Subject: [PATCH 161/398] Trying to get dashboard loading and dashboard meta flags like canSave, canStar more managable --- CHANGELOG.md | 1 + pkg/api/dashboard.go | 4 ++-- pkg/api/dashboard_snapshot.go | 1 + pkg/api/dtos/models.go | 1 + pkg/api/stars.go | 5 +++++ pkg/models/dashboards.go | 7 ++++--- public/app/features/dashboard/dashboardCtrl.js | 2 +- public/app/features/dashboard/dashboardLoaderSrv.js | 8 +++----- public/app/features/dashboard/dashboardSrv.js | 3 +-- .../app/features/dashboard/partials/dashboardTopNav.html | 5 ++--- 10 files changed, 21 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9594718abc..72d97aa2f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ **Breaking changes** - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing +- Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 5b6c839fa97..9de62566d4a 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -53,7 +53,8 @@ func GetDashboard(c *middleware.Context) { IsStarred: isStarred, Slug: slug, Type: m.DashTypeDB, - CanSave: c.OrgRole != m.ROLE_VIEWER, + CanStar: c.IsSignedIn, + CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, }, } @@ -136,7 +137,6 @@ func GetDashboardFromJsonFile(c *middleware.Context) { dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data} dash.Meta.Type = m.DashTypeJson - dash.Meta.CanSave = false c.JSON(200, &dash) } diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 7bdb6807890..0061a2eba19 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -66,6 +66,7 @@ func GetDashboardSnapshot(c *middleware.Context) { dto := dtos.DashboardFullWithMeta{ Dashboard: snapshot.Dashboard, Meta: dtos.DashboardMeta{ + Type: m.DashTypeSnapshot, IsSnapshot: true, Created: snapshot.Created, Expires: snapshot.Expires, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 1d773932a6f..d5e294cc983 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -34,6 +34,7 @@ type DashboardMeta struct { IsSnapshot bool `json:"isSnapshot,omitempty"` Type string `json:"type,omitempty"` CanSave bool `json:"canSave"` + CanStar bool `json:"canStar"` Slug string `json:"slug"` Expires time.Time `json:"expires"` Created time.Time `json:"created"` diff --git a/pkg/api/stars.go b/pkg/api/stars.go index 2b5e64ce8e0..42a096ff4fb 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -7,6 +7,11 @@ import ( ) func StarDashboard(c *middleware.Context) { + if !c.IsSignedIn { + c.JsonApiErr(412, "You need to sign in to star dashboards", nil) + return + } + var cmd = m.StarDashboardCommand{ UserId: c.UserId, DashboardId: c.ParamsInt64(":id"), diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 91db356879a..5d21533f960 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -16,9 +16,10 @@ var ( ) var ( - DashTypeJson = "file" - DashTypeDB = "db" - DashTypeScript = "script" + DashTypeJson = "file" + DashTypeDB = "db" + DashTypeScript = "script" + DashTypeSnapshot = "snapshot" ) // Dashboard model diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ffead046211..3d2b3edbaab 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -70,7 +70,7 @@ function (angular, $, config) { }; $scope.updateTopNavPartial = function() { - if ($scope.dashboard.meta.isSnapshot) { + if ($scope.dashboard.meta.type === 'snapshot') { $scope.topNavPartial = 'app/features/dashboard/partials/snapshotTopNav.html'; } }; diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index 48ca279d221..f2fc3c9c03f 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -19,7 +19,7 @@ function (angular, moment, _, $, kbn) { var self = this; this._dashboardLoadFailed = function(title) { - return {meta: {}, dashboard: {title: title}}; + return {meta: {canStar: false, canDelete: false, canSave: false}, dashboard: {title: title}}; }; this.loadDashboard = function(type, slug) { @@ -28,9 +28,7 @@ function (angular, moment, _, $, kbn) { } if (type === 'snapshot') { - return backendSrv.get('/api/snapshots/' + $routeParams.slug).then(function(result) { - return result; - }, function() { + return backendSrv.get('/api/snapshots/' + $routeParams.slug).catch(function() { return {meta:{isSnapshot: true, canSave: false, canEdit: false}, dashboard: {title: 'Snapshot not found'}}; }); } @@ -45,7 +43,7 @@ function (angular, moment, _, $, kbn) { return $http({ url: url, method: "GET" }) .then(this._executeScript).then(function(result) { - return { meta: { fromScript: true, canDelete: false, canSave: false}, dashboard: result.data }; + return { meta: { fromScript: true, canDelete: false, canSave: false, canStar: false}, dashboard: result.data }; }, function(err) { console.log('Script dashboard error '+ err); $rootScope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 4d79d06675a..9ddce2dbea8 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -56,9 +56,8 @@ function (angular, $, kbn, _, moment) { meta.canShare = meta.canShare === false ? false : true; meta.canSave = meta.canSave === false ? false : true; - meta.canEdit = meta.canEdit === false ? false : true; meta.canStar = meta.canStar === false ? false : true; - meta.canDelete = meta.canDelete === false ? false : true; + meta.canEdit = meta.canEdit === false ? false : true; if (!this.editable) { meta.canEdit = false; diff --git a/public/app/features/dashboard/partials/dashboardTopNav.html b/public/app/features/dashboard/partials/dashboardTopNav.html index 89a6a185759..58445abf505 100644 --- a/public/app/features/dashboard/partials/dashboardTopNav.html +++ b/public/app/features/dashboard/partials/dashboardTopNav.html @@ -15,7 +15,6 @@ - From fb35f7210cef5746c5a4b89e6802d698c908dd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 10:06:06 +0200 Subject: [PATCH 162/398] Fixed failing golang test --- pkg/services/search/json_index_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/search/json_index_test.go b/pkg/services/search/json_index_test.go index ddae1f20ace..a18c19b92a7 100644 --- a/pkg/services/search/json_index_test.go +++ b/pkg/services/search/json_index_test.go @@ -20,7 +20,7 @@ func TestJsonDashIndex(t *testing.T) { res, err := index.Search(&Query{Title: "", Tag: ""}) So(err, ShouldBeNil) - So(len(res), ShouldEqual, 4) + So(len(res), ShouldEqual, 3) }) Convey("Should be able to search index by title", func() { From bb7d79e6d21c1551fd8e9f5595b56e5d6879080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 10:45:53 +0200 Subject: [PATCH 163/398] Refactoring search to support more than just db dashboards --- CHANGELOG.md | 1 + pkg/api/api.go | 1 + pkg/api/dashboard.go | 11 +++++ pkg/api/search.go | 47 +++++-------------- pkg/services/search/json_index.go | 19 ++++++-- pkg/services/sqlstore/dashboard.go | 1 + public/app/controllers/search.js | 18 +++---- .../app/features/dashboard/dashboardCtrl.js | 2 +- public/app/partials/search.html | 19 ++++---- 9 files changed, 61 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d97aa2f98..de405e1232b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ **Breaking changes** - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing - Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) +- Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/api/api.go b/pkg/api/api.go index 7213be5fe87..14e693752b9 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -102,6 +102,7 @@ func Register(r *macaron.Macaron) { r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard) r.Get("/file/:file", GetDashboardFromJsonFile) r.Get("/home", GetHomeDashboard) + r.Get("/tags", GetDashboardTags) }) // Search diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 9de62566d4a..3f05d53f3b5 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -140,3 +140,14 @@ func GetDashboardFromJsonFile(c *middleware.Context) { c.JSON(200, &dash) } + +func GetDashboardTags(c *middleware.Context) { + query := m.GetDashboardTagsQuery{OrgId: c.OrgId} + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Failed to get tags from database", err) + return + } + + c.JSON(200, query.Result) +} diff --git a/pkg/api/search.go b/pkg/api/search.go index 588708e2de9..68b24fca38e 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -3,14 +3,12 @@ package api import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" - m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) func Search(c *middleware.Context) { query := c.Query("query") tag := c.Query("tag") - tagcloud := c.Query("tagcloud") starred := c.Query("starred") limit := c.QueryInt("limit") @@ -18,41 +16,20 @@ func Search(c *middleware.Context) { limit = 200 } - result := m.SearchResult{ - Dashboards: []*m.DashboardSearchHit{}, - Tags: []*m.DashboardTagCloudItem{}, + searchQuery := search.Query{ + Title: query, + Tag: tag, + UserId: c.UserId, + Limit: limit, + IsStarred: starred == "true", + OrgId: c.OrgId, } - if tagcloud == "true" { - - query := m.GetDashboardTagsQuery{OrgId: c.OrgId} - err := bus.Dispatch(&query) - if err != nil { - c.JsonApiErr(500, "Failed to get tags from database", err) - return - } - result.Tags = query.Result - result.TagsOnly = true - - } else { - - query := search.Query{ - Title: query, - Tag: tag, - UserId: c.UserId, - Limit: limit, - IsStarred: starred == "true", - OrgId: c.OrgId, - } - - err := bus.Dispatch(&query) - if err != nil { - c.JsonApiErr(500, "Search failed", err) - return - } - - result.Dashboards = query.Result + err := bus.Dispatch(&searchQuery) + if err != nil { + c.JsonApiErr(500, "Search failed", err) + return } - c.JSON(200, result) + c.JSON(200, searchQuery.Result) } diff --git a/pkg/services/search/json_index.go b/pkg/services/search/json_index.go index c08dece7e71..ac8482f77e1 100644 --- a/pkg/services/search/json_index.go +++ b/pkg/services/search/json_index.go @@ -24,11 +24,10 @@ type JsonDashIndexItem struct { } func NewJsonDashIndex(path string, orgIds string) *JsonDashIndex { + log.Info("Creating json dashboard index for path: ", path) + index := JsonDashIndex{} index.path = path - // if orgIds != "" || orgIds != "*" { - // } - index.updateIndex() return &index } @@ -37,8 +36,21 @@ func (index *JsonDashIndex) Search(query *Query) ([]*m.DashboardSearchHit, error results := make([]*m.DashboardSearchHit, 0) for _, item := range index.items { + if len(results) > query.Limit { + break + } + + // filter out results with tag filter + if query.Tag != "" { + if !strings.Contains(item.TagsCsv, query.Tag) { + continue + } + } + + // add results with matchig title filter if strings.Contains(item.TitleLower, query.Title) { results = append(results, &m.DashboardSearchHit{ + Type: m.DashTypeJson, Title: item.Dashboard.Title, Tags: item.Dashboard.GetTags(), Uri: "file/" + item.Path, @@ -60,7 +72,6 @@ func (index *JsonDashIndex) GetDashboard(path string) *m.Dashboard { } func (index *JsonDashIndex) updateIndex() error { - log.Info("Updating JSON dashboard index, path: %v", index.path) index.items = make([]*JsonDashIndexItem, 0) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 9cd56bab7b5..56e462fe2f6 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -176,6 +176,7 @@ func SearchDashboards(query *m.SearchDashboardsQuery) error { Id: item.Id, Title: item.Title, Uri: "db/" + item.Slug, + Type: m.DashTypeDB, Tags: []string{}, } query.Result = append(query.Result, hit) diff --git a/public/app/controllers/search.js b/public/app/controllers/search.js index c013b829e54..c4785c6dd8f 100644 --- a/public/app/controllers/search.js +++ b/public/app/controllers/search.js @@ -61,21 +61,21 @@ function (angular, _, config) { }; $scope.searchDashboards = function() { + $scope.tagsMode = false; $scope.currentSearchId = $scope.currentSearchId + 1; var localSearchId = $scope.currentSearchId; return backendSrv.search($scope.query).then(function(results) { if (localSearchId < $scope.currentSearchId) { return; } - $scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length; - $scope.results.tags = results.tags; - $scope.results.dashboards = _.map(results.dashboards, function(dash) { + $scope.resultCount = results.length; + $scope.results = _.map(results, function(dash) { dash.url = 'dashboard/' + dash.uri; return dash; }); if ($scope.queryHasNoFilters()) { - $scope.results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); + $scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); } }); }; @@ -96,10 +96,12 @@ function (angular, _, config) { } }; - $scope.showTags = function() { - $scope.query.tagcloud = !$scope.query.tagcloud; - $scope.giveSearchFocus = $scope.giveSearchFocus + 1; - $scope.search(); + $scope.getTags = function() { + $scope.tagsMode = true; + return backendSrv.get('/api/dashboards/tags').then(function(results) { + $scope.resultCount = results.length; + $scope.results = results; + }); }; $scope.showStarred = function() { diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index 3d2b3edbaab..ffead046211 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -70,7 +70,7 @@ function (angular, $, config) { }; $scope.updateTopNavPartial = function() { - if ($scope.dashboard.meta.type === 'snapshot') { + if ($scope.dashboard.meta.isSnapshot) { $scope.topNavPartial = 'app/features/dashboard/partials/snapshotTopNav.html'; } }; diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 85383042a21..ad1dcf0d1ba 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -11,8 +11,8 @@ starred | - - + + tags @@ -25,10 +25,10 @@
    -
    +
    -
    -
    No dashboards matching your query were found.
    +
    +
    No dashboards matching your query were found.
    -
    + @@ -54,8 +54,7 @@ - - + From c8146e759f110e38f31600fdd04c8e67031ae445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 10:57:18 +0200 Subject: [PATCH 164/398] Fixed json index unit test --- pkg/services/search/json_index_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/search/json_index_test.go b/pkg/services/search/json_index_test.go index a18c19b92a7..13add2a667d 100644 --- a/pkg/services/search/json_index_test.go +++ b/pkg/services/search/json_index_test.go @@ -17,14 +17,14 @@ func TestJsonDashIndex(t *testing.T) { }) Convey("Should be able to search index", func() { - res, err := index.Search(&Query{Title: "", Tag: ""}) + res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 3) }) Convey("Should be able to search index by title", func() { - res, err := index.Search(&Query{Title: "home", Tag: ""}) + res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 1) From 448a8b8d1c879988f8c250f8bffe84ec2ad199f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 13:36:13 +0200 Subject: [PATCH 165/398] Major refactorings around searching, moved to seperate package, trying to move stuff out of models package, extend search support searching different types of entities and different types of dashboards, #960 --- conf/defaults.ini | 3 +- conf/sample.ini | 8 +++ main.go | 2 +- pkg/api/dashboard.go | 2 +- pkg/api/search.go | 2 +- pkg/models/dashboards.go | 10 ++++ pkg/models/search.go | 29 +---------- .../search/search.go => search/handlers.go} | 23 +++----- pkg/{services => }/search/json_index.go | 15 +++--- pkg/{services => }/search/json_index_test.go | 2 +- pkg/search/models.go | 47 +++++++++++++++++ pkg/services/sqlstore/dashboard.go | 11 ++-- pkg/services/sqlstore/dashboard_test.go | 9 ++-- public/app/controllers/search.js | 37 ++++--------- public/app/features/dashlinks/module.js | 4 +- public/app/panels/dashlist/module.html | 2 +- public/app/panels/dashlist/module.js | 2 +- public/app/partials/search.html | 52 +++++++++---------- public/css/less/search.less | 11 +++- 19 files changed, 143 insertions(+), 128 deletions(-) rename pkg/{services/search/search.go => search/handlers.go} (78%) rename pkg/{services => }/search/json_index.go (87%) rename pkg/{services => }/search/json_index_test.go (91%) create mode 100644 pkg/search/models.go diff --git a/conf/defaults.ini b/conf/defaults.ini index b4e5a1ddfae..628df5360e4 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -216,7 +216,6 @@ exchange = grafana_events #################################### Dashboard JSON files ########################## [dashboards.json] enabled = false -path = dashboards -orgs = * +path = /var/lib/grafana/dashboards diff --git a/conf/sample.ini b/conf/sample.ini index fc2d5a1c8f2..df204a7f45d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -211,3 +211,11 @@ ;enabled = false ;rabbitmq_url = amqp://localhost/ ;exchange = grafana_events + +;#################################### Dashboard JSON files ########################## +[dashboards.json] +;enabled = false +;path = /var/lib/grafana/dashboards + + + diff --git a/main.go b/main.go index e794e4dfe4e..d31979b5ad2 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/search" "github.com/grafana/grafana/pkg/services/eventpublisher" - "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 3f05d53f3b5..b439bd67ac7 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/api/search.go b/pkg/api/search.go index 68b24fca38e..10329f445bd 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -3,7 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/search" ) func Search(c *middleware.Context) { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 5d21533f960..b802eba7e1e 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -126,3 +126,13 @@ type GetDashboardQuery struct { Result *Dashboard } + +type DashboardTagCloudItem struct { + Term string `json:"term"` + Count int `json:"count"` +} + +type GetDashboardTagsQuery struct { + OrgId int64 + Result []*DashboardTagCloudItem +} diff --git a/pkg/models/search.go b/pkg/models/search.go index 6e6d88ed828..8bd4744f07b 100644 --- a/pkg/models/search.go +++ b/pkg/models/search.go @@ -1,12 +1,6 @@ package models -type SearchResult struct { - Dashboards []*DashboardSearchHit `json:"dashboards"` - Tags []*DashboardTagCloudItem `json:"tags"` - TagsOnly bool `json:"tagsOnly"` -} - -type DashboardSearchHit struct { +type SearchHit struct { Id int64 `json:"id"` Title string `json:"title"` Uri string `json:"uri"` @@ -14,24 +8,3 @@ type DashboardSearchHit struct { Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` } - -type DashboardTagCloudItem struct { - Term string `json:"term"` - Count int `json:"count"` -} - -type SearchDashboardsQuery struct { - Title string - Tag string - OrgId int64 - UserId int64 - Limit int - IsStarred bool - - Result []*DashboardSearchHit -} - -type GetDashboardTagsQuery struct { - OrgId int64 - Result []*DashboardTagCloudItem -} diff --git a/pkg/services/search/search.go b/pkg/search/handlers.go similarity index 78% rename from pkg/services/search/search.go rename to pkg/search/handlers.go index 4ab7d7bd0ba..bbafffb8743 100644 --- a/pkg/services/search/search.go +++ b/pkg/search/handlers.go @@ -2,23 +2,13 @@ package search import ( "path/filepath" + "sort" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) -type Query struct { - Title string - Tag string - OrgId int64 - UserId int64 - Limit int - IsStarred bool - - Result []*m.DashboardSearchHit -} - var jsonDashIndex *JsonDashIndex func Init() { @@ -33,15 +23,14 @@ func Init() { jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath) } - orgIds := jsonIndexCfg.Key("org_ids").String() - jsonDashIndex = NewJsonDashIndex(jsonFilesPath, orgIds) + jsonDashIndex = NewJsonDashIndex(jsonFilesPath) } } func searchHandler(query *Query) error { - hits := make([]*m.DashboardSearchHit, 0) + hits := make(HitList, 0) - dashQuery := m.SearchDashboardsQuery{ + dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, Tag: query.Tag, UserId: query.UserId, @@ -65,6 +54,8 @@ func searchHandler(query *Query) error { hits = append(hits, jsonHits...) } + sort.Sort(hits) + if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { return err } @@ -73,7 +64,7 @@ func searchHandler(query *Query) error { return nil } -func setIsStarredFlagOnSearchResults(userId int64, hits []*m.DashboardSearchHit) error { +func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { query := m.GetUserStarsQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { return err diff --git a/pkg/services/search/json_index.go b/pkg/search/json_index.go similarity index 87% rename from pkg/services/search/json_index.go rename to pkg/search/json_index.go index ac8482f77e1..5a500d2f688 100644 --- a/pkg/services/search/json_index.go +++ b/pkg/search/json_index.go @@ -11,9 +11,8 @@ import ( ) type JsonDashIndex struct { - path string - orgsIds []int64 - items []*JsonDashIndexItem + path string + items []*JsonDashIndexItem } type JsonDashIndexItem struct { @@ -23,7 +22,7 @@ type JsonDashIndexItem struct { Dashboard *m.Dashboard } -func NewJsonDashIndex(path string, orgIds string) *JsonDashIndex { +func NewJsonDashIndex(path string) *JsonDashIndex { log.Info("Creating json dashboard index for path: ", path) index := JsonDashIndex{} @@ -32,8 +31,8 @@ func NewJsonDashIndex(path string, orgIds string) *JsonDashIndex { return &index } -func (index *JsonDashIndex) Search(query *Query) ([]*m.DashboardSearchHit, error) { - results := make([]*m.DashboardSearchHit, 0) +func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { + results := make([]*Hit, 0) for _, item := range index.items { if len(results) > query.Limit { @@ -49,8 +48,8 @@ func (index *JsonDashIndex) Search(query *Query) ([]*m.DashboardSearchHit, error // add results with matchig title filter if strings.Contains(item.TitleLower, query.Title) { - results = append(results, &m.DashboardSearchHit{ - Type: m.DashTypeJson, + results = append(results, &Hit{ + Type: DashHitJson, Title: item.Dashboard.Title, Tags: item.Dashboard.GetTags(), Uri: "file/" + item.Path, diff --git a/pkg/services/search/json_index_test.go b/pkg/search/json_index_test.go similarity index 91% rename from pkg/services/search/json_index_test.go rename to pkg/search/json_index_test.go index 13add2a667d..9c4d27e6233 100644 --- a/pkg/services/search/json_index_test.go +++ b/pkg/search/json_index_test.go @@ -9,7 +9,7 @@ import ( func TestJsonDashIndex(t *testing.T) { Convey("Given the json dash index", t, func() { - index := NewJsonDashIndex("../../../public/dashboards/", "*") + index := NewJsonDashIndex("../../public/dashboards/", "*") Convey("Should be able to update index", func() { err := index.updateIndex() diff --git a/pkg/search/models.go b/pkg/search/models.go new file mode 100644 index 00000000000..157d9a292e3 --- /dev/null +++ b/pkg/search/models.go @@ -0,0 +1,47 @@ +package search + +type HitType string + +const ( + DashHitDB HitType = "dash-db" + DashHitHome HitType = "dash-home" + DashHitJson HitType = "dash-json" + DashHitScripted HitType = "dash-scripted" +) + +type Hit struct { + Id int64 `json:"id"` + Title string `json:"title"` + Uri string `json:"uri"` + Type HitType `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` +} + +type HitList []*Hit + +func (s HitList) Len() int { return len(s) } +func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } + +type Query struct { + Title string + Tag string + OrgId int64 + UserId int64 + Limit int + IsStarred bool + + Result HitList +} + +type FindPersistedDashboardsQuery struct { + Title string + Tag string + OrgId int64 + UserId int64 + Limit int + IsStarred bool + + Result HitList +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 56e462fe2f6..027f2cd2fac 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/search" ) func init() { @@ -119,7 +120,7 @@ type DashboardSearchProjection struct { Term string } -func SearchDashboards(query *m.SearchDashboardsQuery) error { +func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { var sql bytes.Buffer params := make([]interface{}, 0) @@ -166,17 +167,17 @@ func SearchDashboards(query *m.SearchDashboardsQuery) error { return err } - query.Result = make([]*m.DashboardSearchHit, 0) - hits := make(map[int64]*m.DashboardSearchHit) + query.Result = make([]*search.Hit, 0) + hits := make(map[int64]*search.Hit) for _, item := range res { hit, exists := hits[item.Id] if !exists { - hit = &m.DashboardSearchHit{ + hit = &search.Hit{ Id: item.Id, Title: item.Title, Uri: "db/" + item.Slug, - Type: m.DashTypeDB, + Type: search.DashHitDB, Tags: []string{}, } query.Result = append(query.Result, hit) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index ad4974af18b..70675f9c42b 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -6,6 +6,7 @@ import ( . "github.com/smartystreets/goconvey/convey" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/search" ) func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { @@ -85,7 +86,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for dashboard", func() { - query := m.SearchDashboardsQuery{ + query := search.FindPersistedDashboardsQuery{ Title: "test", OrgId: 1, } @@ -99,8 +100,8 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for dashboards using tags", func() { - query1 := m.SearchDashboardsQuery{Tag: "webapp", OrgId: 1} - query2 := m.SearchDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} + query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1} + query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} err := SearchDashboards(&query1) err = SearchDashboards(&query2) @@ -146,7 +147,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for starred dashboards", func() { - query := m.SearchDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true} + query := search.FindPersistedDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true} err := SearchDashboards(&query) So(err, ShouldBeNil) diff --git a/public/app/controllers/search.js b/public/app/controllers/search.js index c4785c6dd8f..9083ef74705 100644 --- a/public/app/controllers/search.js +++ b/public/app/controllers/search.js @@ -40,15 +40,15 @@ function (angular, _, config) { $scope.moveSelection(-1); } if (evt.keyCode === 13) { - if ($scope.query.tagcloud) { - var tag = $scope.results.tags[$scope.selectedIndex]; + if ($scope.tagMode) { + var tag = $scope.results[$scope.selectedIndex]; if (tag) { $scope.filterByTag(tag.term); } return; } - var selectedDash = $scope.results.dashboards[$scope.selectedIndex]; + var selectedDash = $scope.results[$scope.selectedIndex]; if (selectedDash) { $location.search({}); $location.path(selectedDash.url); @@ -57,7 +57,9 @@ function (angular, _, config) { }; $scope.moveSelection = function(direction) { - $scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0); + var max = ($scope.results || []).length; + var newIndex = $scope.selectedIndex + direction; + $scope.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex; }; $scope.searchDashboards = function() { @@ -68,14 +70,13 @@ function (angular, _, config) { return backendSrv.search($scope.query).then(function(results) { if (localSearchId < $scope.currentSearchId) { return; } - $scope.resultCount = results.length; $scope.results = _.map(results, function(dash) { dash.url = 'dashboard/' + dash.uri; return dash; }); if ($scope.queryHasNoFilters()) { - $scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); + $scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' }); } }); }; @@ -97,10 +98,10 @@ function (angular, _, config) { }; $scope.getTags = function() { - $scope.tagsMode = true; return backendSrv.get('/api/dashboards/tags').then(function(results) { - $scope.resultCount = results.length; + $scope.tagsMode = true; $scope.results = results; + $scope.giveSearchFocus = $scope.giveSearchFocus + 1; }); }; @@ -116,26 +117,6 @@ function (angular, _, config) { $scope.searchDashboards(); }; - $scope.addMetricToCurrentDashboard = function (metricId) { - $scope.dashboard.rows.push({ - title: '', - height: '250px', - editable: true, - panels: [ - { - type: 'graphite', - title: 'test', - span: 12, - targets: [{ target: metricId }] - } - ] - }); - }; - - $scope.toggleImport = function () { - $scope.showImport = !$scope.showImport; - }; - $scope.newDashboard = function() { $location.url('dashboard/new'); }; diff --git a/public/app/features/dashlinks/module.js b/public/app/features/dashlinks/module.js index fbf5104799c..e33d5406fc2 100644 --- a/public/app/features/dashlinks/module.js +++ b/public/app/features/dashlinks/module.js @@ -133,12 +133,12 @@ function (angular, _) { $scope.searchDashboards = function(link) { return backendSrv.search({tag: link.tag}).then(function(results) { - return _.reduce(results.dashboards, function(memo, dash) { + return _.reduce(results, function(memo, dash) { // do not add current dashboard if (dash.id !== currentDashId) { memo.push({ title: dash.title, - url: 'dashboard/db/'+ dash.slug, + url: 'dashboard/' + dash.uri, icon: 'fa fa-th-large', keepTime: link.keepTime, includeVars: link.includeVars diff --git a/public/app/panels/dashlist/module.html b/public/app/panels/dashlist/module.html index a7b114b31d2..de0e23a9ea5 100644 --- a/public/app/panels/dashlist/module.html +++ b/public/app/panels/dashlist/module.html @@ -1,7 +1,7 @@ -
    -
    -
    -
    - + -
    -
    No dashboards matching your query were found.
    +
    +
    No dashboards matching your query were found.
    - + - - - {{tag}} - - + + + {{tag}} + + - - - - - -
    + + + + +
    diff --git a/public/css/less/search.less b/public/css/less/search.less index 7182333bd16..b72a7ca0e70 100644 --- a/public/css/less/search.less +++ b/public/css/less/search.less @@ -41,7 +41,7 @@ display: block; line-height: 28px; - .search-result-item:hover, .search-result-item.selected { + .search-item:hover, .search-item.selected { background-color: @grafanaListHighlight; } @@ -67,12 +67,19 @@ } } - .search-result-item { + .search-item { display: block; padding: 3px 10px; white-space: nowrap; background-color: @grafanaListBackground; margin-bottom: 4px; + .search-result-icon:before { + content: "\f009"; + } + + &.search-item-dash-home .search-result-icon:before { + content: "\f015"; + } } .search-result-tags { From 1a401780bab7a73a7c96ddaad65ec16ea0181c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 14:01:42 +0200 Subject: [PATCH 166/398] Updated changelog with completed story, JSON dashboards added to search, Closes #960 --- CHANGELOG.md | 1 + pkg/search/handlers.go | 1 + pkg/search/json_index.go | 35 +++++++++++++++++++++-------------- pkg/search/json_index_test.go | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de405e1232b..5c5ada93d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images - [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER) +- [Issue #960](https://github.com/grafana/grafana/issues/960). Search: Backend can now index a folder with json files, will be available in search (saving back to folder is not supported, this feature is meant for static generated json dashboards) **Breaking changes** - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index bbafffb8743..874b85994ca 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -24,6 +24,7 @@ func Init() { } jsonDashIndex = NewJsonDashIndex(jsonFilesPath) + go jsonDashIndex.updateLoop() } } diff --git a/pkg/search/json_index.go b/pkg/search/json_index.go index 5a500d2f688..0b4c335e703 100644 --- a/pkg/search/json_index.go +++ b/pkg/search/json_index.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" @@ -23,7 +24,7 @@ type JsonDashIndexItem struct { } func NewJsonDashIndex(path string) *JsonDashIndex { - log.Info("Creating json dashboard index for path: ", path) + log.Info("Creating json dashboard index for path: %v", path) index := JsonDashIndex{} index.path = path @@ -31,6 +32,18 @@ func NewJsonDashIndex(path string) *JsonDashIndex { return &index } +func (index *JsonDashIndex) updateLoop() { + ticker := time.NewTicker(time.Minute) + for { + select { + case <-ticker.C: + if err := index.updateIndex(); err != nil { + log.Error(3, "Failed to update dashboard json index %v", err) + } + } + } +} + func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { results := make([]*Hit, 0) @@ -71,8 +84,7 @@ func (index *JsonDashIndex) GetDashboard(path string) *m.Dashboard { } func (index *JsonDashIndex) updateIndex() error { - - index.items = make([]*JsonDashIndexItem, 0) + var items = make([]*JsonDashIndexItem, 0) visitor := func(path string, f os.FileInfo, err error) error { if err != nil { @@ -81,12 +93,16 @@ func (index *JsonDashIndex) updateIndex() error { if f.IsDir() { return nil } + if strings.HasSuffix(f.Name(), ".json") { - err = index.loadDashboardIntoCache(path) + dash, err := loadDashboardFromFile(path) if err != nil { return err } + + items = append(items, dash) } + return nil } @@ -94,16 +110,7 @@ func (index *JsonDashIndex) updateIndex() error { return err } - return nil -} - -func (index *JsonDashIndex) loadDashboardIntoCache(filename string) error { - dash, err := loadDashboardFromFile(filename) - if err != nil { - return err - } - - index.items = append(index.items, dash) + index.items = items return nil } diff --git a/pkg/search/json_index_test.go b/pkg/search/json_index_test.go index 9c4d27e6233..38fbf7207d9 100644 --- a/pkg/search/json_index_test.go +++ b/pkg/search/json_index_test.go @@ -9,7 +9,7 @@ import ( func TestJsonDashIndex(t *testing.T) { Convey("Given the json dash index", t, func() { - index := NewJsonDashIndex("../../public/dashboards/", "*") + index := NewJsonDashIndex("../../public/dashboards/") Convey("Should be able to update index", func() { err := index.updateIndex() From ea54f6923bb134e7e1acecb5690bfeea9490f51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 16:32:33 +0200 Subject: [PATCH 167/398] Added graphite function changed, closes #1961 --- public/app/plugins/datasource/graphite/gfunc.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/app/plugins/datasource/graphite/gfunc.js b/public/app/plugins/datasource/graphite/gfunc.js index 063fc473f88..4730719d1b5 100644 --- a/public/app/plugins/datasource/graphite/gfunc.js +++ b/public/app/plugins/datasource/graphite/gfunc.js @@ -327,6 +327,13 @@ function (_, $) { defaultParams: [100] }); + addFuncDef({ + name: "changed", + category: categories.Special, + params: [], + defaultParams: [] + }); + addFuncDef({ name: 'scale', category: categories.Transform, From bfafb836049cb28db57678a6db1b584f19640095 Mon Sep 17 00:00:00 2001 From: jeff martinez Date: Wed, 13 May 2015 09:58:58 -0700 Subject: [PATCH 168/398] Fix minor grammar issue in docs index page --- docs/sources/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/index.md b/docs/sources/index.md index 24ba087575e..fe367c7f87a 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -4,7 +4,7 @@ page_keywords: grafana, introduction, documentation, about # About Grafana -Grafana is a leading open source applications for visualizing large-scale measurement data. +Grafana is a leading open source application for visualizing large-scale measurement data. It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world. From 2dac9758053f514a7160fa14d35a04ae18de5b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2015 21:34:50 +0200 Subject: [PATCH 169/398] began work on color by regex / series override for color, #590 --- .../app/panels/graph/seriesOverridesCtrl.js | 28 +++++++++++++++++-- public/app/panels/graph/styleEditor.html | 4 ++- public/app/partials/colorpicker.html | 11 ++++++++ public/app/services/popoverSrv.js | 10 ++++++- public/css/less/graph.less | 1 + 5 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 public/app/partials/colorpicker.html diff --git a/public/app/panels/graph/seriesOverridesCtrl.js b/public/app/panels/graph/seriesOverridesCtrl.js index 80fb2ead6c2..6105402ce05 100644 --- a/public/app/panels/graph/seriesOverridesCtrl.js +++ b/public/app/panels/graph/seriesOverridesCtrl.js @@ -1,14 +1,15 @@ define([ 'angular', + 'jquery', 'app', 'lodash', -], function(angular, app, _) { +], function(angular, jquery, app, _) { 'use strict'; var module = angular.module('grafana.panels.graph', []); app.useModule(module); - module.controller('SeriesOverridesCtrl', function($scope) { + module.controller('SeriesOverridesCtrl', function($scope, $element, popoverSrv, $timeout) { $scope.overrideMenu = []; $scope.currentOverrides = []; $scope.override = $scope.override || {}; @@ -37,10 +38,32 @@ define([ $scope.addSeriesOverride({ alias: subItem.value, lines: false }); } + if (item.propertyName === 'color') { + $scope.openColorSelector(); + } + $scope.updateCurrentOverrides(); $scope.render(); }; + $scope.colorSelected = function(color) { + $scope.override['color'] = color; + $scope.updateCurrentOverrides(); + $scope.render(); + }; + + $scope.openColorSelector = function() { + var popoverScope = $scope.$new(); + popoverScope.colorSelected = $scope.colorSelected; + + popoverSrv.show({ + element: $element.find(".dropdown"), + placement: 'top', + templateUrl: 'app/partials/colorpicker.html', + scope: popoverScope + }); + }; + $scope.removeOverride = function(option) { delete $scope.override[option.propertyName]; $scope.updateCurrentOverrides(); @@ -75,6 +98,7 @@ define([ $scope.addOverrideOption('Points', 'points', [true, false]); $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]); $scope.addOverrideOption('Stack', 'stack', [true, false, 2, 3, 4, 5]); + $scope.addOverrideOption('Color', 'color', ['change']); $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]); $scope.updateCurrentOverrides(); diff --git a/public/app/panels/graph/styleEditor.html b/public/app/panels/graph/styleEditor.html index a5d82bba262..d646b766f70 100644 --- a/public/app/panels/graph/styleEditor.html +++ b/public/app/panels/graph/styleEditor.html @@ -94,6 +94,8 @@
    - +
    diff --git a/public/app/partials/colorpicker.html b/public/app/partials/colorpicker.html new file mode 100644 index 00000000000..f687b740bbf --- /dev/null +++ b/public/app/partials/colorpicker.html @@ -0,0 +1,11 @@ +
    + × + +
    +   +
    + +
    + diff --git a/public/app/services/popoverSrv.js b/public/app/services/popoverSrv.js index a1e60525e2d..0fb94b06ebe 100644 --- a/public/app/services/popoverSrv.js +++ b/public/app/services/popoverSrv.js @@ -21,12 +21,20 @@ function (angular, _) { return; } + options.scope.dismiss = function() { + popover = options.element.data('popover'); + if (popover) { + popover.destroy(); + } + options.scope.$destroy(); + }; + this.getTemplate(options.templateUrl).then(function(result) { var template = _.isString(result) ? result : result.data; options.element.popover({ content: template, - placement: 'bottom', + placement: options.placement || 'bottom', html: true }); diff --git a/public/css/less/graph.less b/public/css/less/graph.less index a56fe968017..1b29816fa12 100644 --- a/public/css/less/graph.less +++ b/public/css/less/graph.less @@ -164,6 +164,7 @@ .graph-legend-popover { width: 200px; + min-height: 100px; label { display: inline-block; } From 1c9993f185906fcde01990864c6d453ba4462140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 May 2015 10:15:46 +0200 Subject: [PATCH 170/398] Backend configuration: When unknown key was detected the log error was not visible in log file due to logging was initialized after user config file is loaded, logging is now initialized twice, once based on defaults and command line arguments, then then again after user config file, Fixes #1992 --- pkg/setting/setting.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 389b6b933b9..77445dd6c4c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -289,10 +289,13 @@ func loadConfiguration(args *CommandLineArgs) { // command line props commandLineProps := getCommandLineProperties(args.Args) - // load default overrides applyCommandLineDefaultProperties(commandLineProps) + // init logging before specific config so we can log errors from here on + DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath) + initLogging(args) + // load specified config file loadSpecifedConfigFile(args.Config) @@ -304,6 +307,10 @@ func loadConfiguration(args *CommandLineArgs) { // evaluate config values containing environment variables evalConfigValues() + + // update data path and logging config + DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath) + initLogging(args) } func pathExists(path string) bool { @@ -339,9 +346,6 @@ func NewConfigContext(args *CommandLineArgs) { setHomePath(args) loadConfiguration(args) - DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath) - initLogging(args) - AppName = Cfg.Section("").Key("app_name").MustString("Grafana") Env = Cfg.Section("").Key("app_mode").MustString("development") From fb56bc59f2483bc05d88f3f50e3bc0d384ecb076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 May 2015 10:33:56 +0200 Subject: [PATCH 171/398] Create pid file before runtime init, Fixes #1990 --- docker/rpmtest/Dockerfile | 6 ------ main.go | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 docker/rpmtest/Dockerfile diff --git a/docker/rpmtest/Dockerfile b/docker/rpmtest/Dockerfile deleted file mode 100644 index 3c18cfa1797..00000000000 --- a/docker/rpmtest/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM centos:latest - -RUN yum install -y initscripts - -ADD *.rpm /tmp/ - diff --git a/main.go b/main.go index d31979b5ad2..5d5bddb7273 100644 --- a/main.go +++ b/main.go @@ -49,8 +49,8 @@ func main() { flag.Parse() - initRuntime() writePIDFile() + initRuntime() search.Init() social.NewOAuthService() From 8c0e1060e628696e3eca078cd06f77fb74cac763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 May 2015 12:34:30 +0200 Subject: [PATCH 172/398] Graph: Define series color using regex rule, Closes #590 --- CHANGELOG.md | 1 + package.json | 2 +- public/app/components/timeSeries.js | 1 + public/app/panels/graph/legend.js | 2 +- public/app/panels/graph/legend.popover.html | 3 +- .../app/panels/graph/seriesOverridesCtrl.js | 12 +++-- public/app/panels/graph/styleEditor.html | 17 +++---- public/app/services/popoverSrv.js | 47 +++++++++++-------- public/css/less/graph.less | 2 +- .../test/specs/seriesOverridesCtrl-specs.js | 6 ++- 10 files changed, 54 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5ada93d58..dbf01ab78c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [Issue #1922](https://github.com/grafana/grafana/issues/1922). Templating: Specify multiple variable values via URL params. - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value - [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards +- [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). diff --git a/package.json b/package.json index 5016a6309eb..71eb75b732b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "karma-coverage": "0.3.1", "karma-coveralls": "0.1.5", "karma-expect": "~1.1.0", - "karma-mocha": "~0.1.4", + "karma-mocha": "~0.1.10", "karma-phantomjs-launcher": "0.1.4", "karma-requirejs": "0.2.2", "karma-script-launcher": "0.1.0", diff --git a/public/app/components/timeSeries.js b/public/app/components/timeSeries.js index c356ddea63b..679777bfb92 100644 --- a/public/app/components/timeSeries.js +++ b/public/app/components/timeSeries.js @@ -53,6 +53,7 @@ function (_, kbn) { if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; } if (override.zindex !== void 0) { this.zindex = override.zindex; } if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; } + if (override.color !== void 0) { this.color = override.color; } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; diff --git a/public/app/panels/graph/legend.js b/public/app/panels/graph/legend.js index fb07275310d..1f016f2c3a3 100644 --- a/public/app/panels/graph/legend.js +++ b/public/app/panels/graph/legend.js @@ -41,7 +41,7 @@ function (angular, app, _, kbn, $) { var popoverScope = scope.$new(); popoverScope.series = seriesInfo; popoverSrv.show({ - element: $(':first-child', el), + element: el, templateUrl: 'app/panels/graph/legend.popover.html', scope: popoverScope }); diff --git a/public/app/panels/graph/legend.popover.html b/public/app/panels/graph/legend.popover.html index 4d24136309e..5410af2d1d1 100644 --- a/public/app/panels/graph/legend.popover.html +++ b/public/app/panels/graph/legend.popover.html @@ -16,8 +16,7 @@
    -   diff --git a/public/app/panels/graph/seriesOverridesCtrl.js b/public/app/panels/graph/seriesOverridesCtrl.js index 6105402ce05..81cb41a8572 100644 --- a/public/app/panels/graph/seriesOverridesCtrl.js +++ b/public/app/panels/graph/seriesOverridesCtrl.js @@ -9,7 +9,7 @@ define([ var module = angular.module('grafana.panels.graph', []); app.useModule(module); - module.controller('SeriesOverridesCtrl', function($scope, $element, popoverSrv, $timeout) { + module.controller('SeriesOverridesCtrl', function($scope, $element, popoverSrv) { $scope.overrideMenu = []; $scope.currentOverrides = []; $scope.override = $scope.override || {}; @@ -29,6 +29,12 @@ define([ }; $scope.setOverride = function(item, subItem) { + // handle color overrides + if (item.propertyName === 'color') { + $scope.openColorSelector(); + return; + } + $scope.override[item.propertyName] = subItem.value; // automatically disable lines for this series and the fill bellow to series @@ -38,10 +44,6 @@ define([ $scope.addSeriesOverride({ alias: subItem.value, lines: false }); } - if (item.propertyName === 'color') { - $scope.openColorSelector(); - } - $scope.updateCurrentOverrides(); $scope.render(); }; diff --git a/public/app/panels/graph/styleEditor.html b/public/app/panels/graph/styleEditor.html index d646b766f70..5d5f2fd7401 100644 --- a/public/app/panels/graph/styleEditor.html +++ b/public/app/panels/graph/styleEditor.html @@ -73,22 +73,23 @@
  • alias or regex
  • +
  • - +
  • +
  • - {{option.name}}: {{option.value}} + + Color: + + + {{option.name}}: {{option.value}} +
  • -
    diff --git a/public/app/services/popoverSrv.js b/public/app/services/popoverSrv.js index 0fb94b06ebe..cec294178c0 100644 --- a/public/app/services/popoverSrv.js +++ b/public/app/services/popoverSrv.js @@ -1,8 +1,9 @@ define([ 'angular', 'lodash', + 'jquery', ], -function (angular, _) { +function (angular, _, $) { 'use strict'; var module = angular.module('grafana.services'); @@ -14,12 +15,16 @@ function (angular, _) { }; this.show = function(options) { - var popover = options.element.data('popover'); - if (popover) { - popover.scope.$destroy(); - popover.destroy(); - return; - } + var popover; + + // hide other popovers + $('.popover').each(function() { + popover = $(this).prev().data('popover'); + if (popover) { + popover.scope.$destroy(); + popover.destroy(); + } + }); options.scope.dismiss = function() { popover = options.element.data('popover'); @@ -30,22 +35,24 @@ function (angular, _) { }; this.getTemplate(options.templateUrl).then(function(result) { - var template = _.isString(result) ? result : result.data; + $timeout(function() { + var template = _.isString(result) ? result : result.data; - options.element.popover({ - content: template, - placement: options.placement || 'bottom', - html: true - }); + options.element.popover({ + content: template, + placement: options.placement || 'bottom', + html: true + }); - popover = options.element.data('popover'); - popover.hasContent = function () { - return template; - }; + popover = options.element.data('popover'); + popover.hasContent = function () { + return template; + }; - popover.toggle(); - popover.scope = options.scope; - $compile(popover.$tip)(popover.scope); + popover.toggle(); + popover.scope = options.scope; + $compile(popover.$tip)(popover.scope); + }, 1); }); }; diff --git a/public/css/less/graph.less b/public/css/less/graph.less index 1b29816fa12..b96e466eea1 100644 --- a/public/css/less/graph.less +++ b/public/css/less/graph.less @@ -114,7 +114,7 @@ th { text-align: right; - padding: 5px 10px; + padding: 0px 10px 1px 0; font-weight: bold; color: @blue; font-size: 85%; diff --git a/public/test/specs/seriesOverridesCtrl-specs.js b/public/test/specs/seriesOverridesCtrl-specs.js index ec7cc70159c..7386083e87b 100644 --- a/public/test/specs/seriesOverridesCtrl-specs.js +++ b/public/test/specs/seriesOverridesCtrl-specs.js @@ -6,11 +6,15 @@ define([ describe('SeriesOverridesCtrl', function() { var ctx = new helpers.ControllerTestContext(); + var popoverSrv = {}; beforeEach(module('grafana.services')); beforeEach(module('grafana.panels.graph')); - beforeEach(ctx.providePhase()); + beforeEach(ctx.providePhase({ + popoverSrv: popoverSrv + })); + beforeEach(ctx.createControllerPhase('SeriesOverridesCtrl')); beforeEach(function() { ctx.scope.render = function() {}; From a258e2e60842398d0f286ad160ff248775693f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 May 2015 17:39:31 +0200 Subject: [PATCH 173/398] Added log base 2 graph y axis scale --- public/app/panels/graph/graph.js | 2 +- public/app/panels/graph/module.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index e63bd6536c3..e090ce4e85e 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -369,7 +369,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { if (_.findWhere(data, {yaxis: 2})) { var secondY = _.clone(defaults); secondY.index = 2, - secondY.logBase = scope.panel.grid.rightLogBase || 2, + secondY.logBase = scope.panel.grid.rightLogBase || 1, secondY.position = 'right'; secondY.min = scope.panel.grid.rightMin; secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax; diff --git a/public/app/panels/graph/module.js b/public/app/panels/graph/module.js index 6671ef33289..867f864123c 100644 --- a/public/app/panels/graph/module.js +++ b/public/app/panels/graph/module.js @@ -116,7 +116,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { _.defaults($scope.panel.grid, _d.grid); _.defaults($scope.panel.legend, _d.legend); - $scope.logScales = {'linear': 1, 'log (base 10)': 10, 'log (base 32)': 32, 'log (base 1024)': 1024}; + $scope.logScales = {'linear': 1, 'log (base 2)': 2, 'log (base 10)': 10, 'log (base 32)': 32, 'log (base 1024)': 1024}; $scope.hiddenSeries = {}; $scope.seriesList = []; From 11c8e80ea9f0e4474507b9b7aa912aba25362161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 11:38:22 +0200 Subject: [PATCH 174/398] Working on new query editor for influxdb 0.9, #1525 --- public/app/directives/all.js | 2 +- .../{graphiteSegment.js => metric.segment.js} | 13 +- .../graphite/partials/query.editor.html | 4 +- .../plugins/datasource/graphite/queryCtrl.js | 17 +- .../plugins/datasource/influxdb/funcEditor.js | 2 +- .../influxdb/partials/query.editor.html | 59 +++++-- .../plugins/datasource/influxdb/queryCtrl.js | 147 +++++++----------- public/css/less/grafana.less | 7 + public/test/specs/graphiteTargetCtrl-specs.js | 6 +- 9 files changed, 132 insertions(+), 125 deletions(-) rename public/app/directives/{graphiteSegment.js => metric.segment.js} (92%) diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 4ab92d111d6..cd294c3df25 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -12,7 +12,7 @@ define([ './bootstrap-tagsinput', './bodyClass', './variableValueSelect', - './graphiteSegment', + './metric.segment', './grafanaVersionCheck', './dropdown.typeahead', './topnav', diff --git a/public/app/directives/graphiteSegment.js b/public/app/directives/metric.segment.js similarity index 92% rename from public/app/directives/graphiteSegment.js rename to public/app/directives/metric.segment.js index c8ad131e6c7..5510dea6657 100644 --- a/public/app/directives/graphiteSegment.js +++ b/public/app/directives/metric.segment.js @@ -9,7 +9,7 @@ function (angular, app, _, $) { angular .module('grafana.directives') - .directive('graphiteSegment', function($compile, $sce) { + .directive('metricSegment', function($compile, $sce) { var inputTemplate = ''; @@ -17,6 +17,12 @@ function (angular, app, _, $) { var buttonTemplate = ''; return { + scope: { + segment: "=", + getAltSegments: "&", + onValueChanged: "&" + }, + link: function($scope, elem) { var $input = $(inputTemplate); var $button = $(buttonTemplate); @@ -46,7 +52,7 @@ function (angular, app, _, $) { segment.expandable = true; segment.fake = false; } - $scope.segmentValueChanged(segment, $scope.$index); + $scope.onValueChanged(); }); }; @@ -69,7 +75,8 @@ function (angular, app, _, $) { if (options) { return options; } $scope.$apply(function() { - $scope.getAltSegments($scope.$index).then(function() { + $scope.getAltSegments().then(function(altSegments) { + $scope.altSegments = altSegments; options = _.map($scope.altSegments, function(alt) { return alt.value; }); // add custom values diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 48446049e65..dcbbdc69a8a 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -74,7 +74,9 @@ ng-show="showTextEditor" /> - + +
    diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index 00e12ea05bd..e78c100b5c5 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,19 +7,39 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) { + + $scope.functionList = [ + 'count', 'mean', 'sum', 'min', + 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', + 'difference' + ]; + + $scope.functionMenu = _.map($scope.functionList, function(func) { + return { text: func, click: "changeFunction('" + func + "');" }; + }); $scope.init = function() { - $scope.segments = $scope.target.segments || []; + var target = $scope.target; + target.function = target.function || 'mean'; - $scope.functionsSelect = [ - 'count', 'mean', 'sum', 'min', - 'max', 'mode', 'distinct', 'median', - 'derivative', 'stddev', 'first', 'last', - 'difference' - ]; + if (!target.measurement) { + $scope.measurementSegment = MetricSegment.newSelectMeasurement(); + } else { + $scope.measurementSegment = new MetricSegment(target.measurement); + } + }; - checkOtherSegments(0); + $scope.changeFunction = function(func) { + $scope.target.function = func; + $scope.$parent.get_data(); + }; + + $scope.measurementChanged = function() { + $scope.target.measurement = $scope.measurementSegment.value; + console.log('measurement updated', $scope.target.measurement); + $scope.$parent.get_data(); }; $scope.toggleQueryMode = function () { @@ -35,103 +55,44 @@ function (angular, _) { $scope.panel.targets.push(clone); }; - $scope.getAltSegments = function (index) { - $scope.altSegments = []; - - var measurement = $scope.segments[0].value; - var queryType, query; - if (index === 0) { - queryType = 'MEASUREMENTS'; - query = 'SHOW MEASUREMENTS'; - } else if (index % 2 === 1) { - queryType = 'TAG_KEYS'; - query = 'SHOW TAG KEYS FROM "' + measurement + '"'; - } else { - queryType = 'TAG_VALUES'; - query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; - } - - console.log('getAltSegments: query' , query); - - return $scope.datasource.metricFindQuery(query, queryType).then(function(results) { + $scope.getMeasurements = function () { + // var measurement = $scope.segments[0].value; + // var queryType, query; + // if (index === 0) { + // queryType = 'MEASUREMENTS'; + // query = 'SHOW MEASUREMENTS'; + // } else if (index % 2 === 1) { + // queryType = 'TAG_KEYS'; + // query = 'SHOW TAG KEYS FROM "' + measurement + '"'; + // } else { + // queryType = 'TAG_VALUES'; + // query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; + // } + // + // console.log('getAltSegments: query' , query); + // + console.log('get measurements'); + return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) { console.log('get alt segments: response', results); - $scope.altSegments = _.map(results, function(segment) { + var measurements = _.map(results, function(segment) { return new MetricSegment({ value: segment.text, expandable: segment.expandable }); }); _.each(templateSrv.variables, function(variable) { - $scope.altSegments.unshift(new MetricSegment({ + measurements.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true, })); }); + + return measurements; }, function(err) { $scope.parserError = err.message || 'Failed to issue metric query'; + return []; }); }; - $scope.segmentValueChanged = function (segment, segmentIndex) { - delete $scope.parserError; - - if (segment.expandable) { - return checkOtherSegments(segmentIndex + 1).then(function () { - setSegmentFocus(segmentIndex + 1); - $scope.targetChanged(); - }); - } - else { - $scope.segments = $scope.segments.splice(0, segmentIndex + 1); - } - - setSegmentFocus(segmentIndex + 1); - $scope.targetChanged(); - }; - - $scope.targetChanged = function() { - if ($scope.parserError) { - return; - } - - $scope.target.measurement = ''; - $scope.target.tags = {}; - $scope.target.measurement = $scope.segments[0].value; - - for (var i = 1; i+1 < $scope.segments.length; i += 2) { - var key = $scope.segments[i].value; - $scope.target.tags[key] = $scope.segments[i+1].value; - } - - $scope.$parent.get_data(); - }; - - function checkOtherSegments(fromIndex) { - if (fromIndex === 0) { - $scope.segments.push(MetricSegment.newSelectMetric()); - return; - } - - if ($scope.segments.length === 0) { - throw('should always have a scope segment?'); - } - - if (_.last($scope.segments).fake) { - return $q.when([]); - } else if ($scope.segments.length % 2 === 1) { - $scope.segments.push(MetricSegment.newSelectTag()); - return $q.when([]); - } else { - $scope.segments.push(MetricSegment.newSelectTagValue()); - return $q.when([]); - } - } - - function setSegmentFocus(segmentIndex) { - _.each($scope.segments, function(segment, index) { - segment.focus = segmentIndex === index; - }); - } - function MetricSegment(options) { if (options === '*' || options.value === '*') { this.value = '*'; @@ -153,8 +114,8 @@ function (angular, _) { this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } - MetricSegment.newSelectMetric = function() { - return new MetricSegment({value: 'select metric', fake: true}); + MetricSegment.newSelectMeasurement = function() { + return new MetricSegment({value: 'select measurement', fake: true}); }; MetricSegment.newSelectTag = function() { diff --git a/public/css/less/grafana.less b/public/css/less/grafana.less index 9efdf3dd360..9be157f80d0 100644 --- a/public/css/less/grafana.less +++ b/public/css/less/grafana.less @@ -337,3 +337,10 @@ text-overflow: ellipsis; } } + +.query-keyword { + font-weight: bold; + color: @blue; +} + + diff --git a/public/test/specs/graphiteTargetCtrl-specs.js b/public/test/specs/graphiteTargetCtrl-specs.js index a5217376098..31916cc2802 100644 --- a/public/test/specs/graphiteTargetCtrl-specs.js +++ b/public/test/specs/graphiteTargetCtrl-specs.js @@ -141,13 +141,15 @@ define([ ctx.scope.target.target = 'test.count'; ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); ctx.scope.init(); - ctx.scope.getAltSegments(1); + ctx.scope.getAltSegments(1).then(function(results) { + ctx.altSegments = results; + }); ctx.scope.$digest(); ctx.scope.$parent = { get_data: sinon.spy() }; }); it('should have no segments', function() { - expect(ctx.scope.altSegments.length).to.be(0); + expect(ctx.altSegments.length).to.be(0); }); }); From 5ca8d590bd9a769397afc24de5529f8b1c9ad429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 15:58:07 +0200 Subject: [PATCH 175/398] Working on new query editor for influxdb 0.9, looking good! #1525 --- public/app/directives/metric.segment.js | 3 +- .../influxdb/partials/query.editor.html | 13 +- .../datasource/influxdb/queryBuilder.js | 4 +- .../plugins/datasource/influxdb/queryCtrl.js | 177 ++++++++++++++---- .../test/specs/influx09-querybuilder-specs.js | 2 +- public/test/specs/influxdbQueryCtrl-specs.js | 120 ++++++++++++ public/test/test-main.js | 1 + 7 files changed, 270 insertions(+), 50 deletions(-) create mode 100644 public/test/specs/influxdbQueryCtrl-specs.js diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index 5510dea6657..05bf7e485e3 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -14,7 +14,8 @@ function (angular, app, _, $) { ' class="tight-form-clear-input input-medium"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = ''; + var buttonTemplate = ''; return { scope: { diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 7210cfc24dc..494eaff5520 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -83,15 +83,16 @@
  • WHERE
  • -
  • - +
  • +
  • GROUP BY - time($interval), + time($interval) +
  • +
  • +
  • -
  • fill (null)
  • fill (0)
  • - + diff --git a/public/app/plugins/datasource/influxdb/queryBuilder.js b/public/app/plugins/datasource/influxdb/queryBuilder.js index e8eaefda61a..9317cea4574 100644 --- a/public/app/plugins/datasource/influxdb/queryBuilder.js +++ b/public/app/plugins/datasource/influxdb/queryBuilder.js @@ -33,8 +33,8 @@ function (_) { query += aggregationFunc + '(value)'; query += ' FROM ' + measurement + ' WHERE $timeFilter'; - query += _.map(target.tags, function(value, key) { - return ' AND ' + key + '=' + "'" + value + "'"; + query += _.map(target.tags, function(tag) { + return ' AND ' + tag.key + '=' + "'" + tag.value + "'"; }).join(''); query += ' GROUP BY time($interval)'; diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index e78c100b5c5..5aeb68f00e7 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,13 +7,11 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { $scope.functionList = [ - 'count', 'mean', 'sum', 'min', - 'max', 'mode', 'distinct', 'median', - 'derivative', 'stddev', 'first', 'last', - 'difference' + 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', 'difference' ]; $scope.functionMenu = _.map($scope.functionList, function(func) { @@ -23,12 +21,41 @@ function (angular, _) { $scope.init = function() { var target = $scope.target; target.function = target.function || 'mean'; + target.tags = target.tags || []; + target.groupByTags = target.groupByTags || []; if (!target.measurement) { $scope.measurementSegment = MetricSegment.newSelectMeasurement(); } else { $scope.measurementSegment = new MetricSegment(target.measurement); } + + $scope.tagSegments = []; + _.each(target.tags, function(tag) { + if (tag.condition) { + $scope.tagSegments.push(MetricSegment.newCondition(tag.condition)); + } + $scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key' })); + $scope.tagSegments.push(new MetricSegment({fake: true, value: "="})); + $scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value'})); + }); + + if ($scope.tagSegments.length % 3 === 0) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + + $scope.groupBySegments = []; + _.each(target.groupByTags, function(tag) { + $scope.groupBySegments.push(new MetricSegment(tag)); + }); + + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + }; + + $scope.groupByTagUpdated = function(segment, index) { + if (index === $scope.groupBySegments.length-1) { + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + } }; $scope.changeFunction = function(func) { @@ -56,43 +83,107 @@ function (angular, _) { }; $scope.getMeasurements = function () { - // var measurement = $scope.segments[0].value; - // var queryType, query; - // if (index === 0) { - // queryType = 'MEASUREMENTS'; - // query = 'SHOW MEASUREMENTS'; - // } else if (index % 2 === 1) { - // queryType = 'TAG_KEYS'; - // query = 'SHOW TAG KEYS FROM "' + measurement + '"'; - // } else { - // queryType = 'TAG_VALUES'; - // query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; - // } - // - // console.log('getAltSegments: query' , query); - // - console.log('get measurements'); - return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) { - console.log('get alt segments: response', results); - var measurements = _.map(results, function(segment) { - return new MetricSegment({ value: segment.text, expandable: segment.expandable }); - }); + return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS') + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(null, $scope.handleQueryError); + }; - _.each(templateSrv.variables, function(variable) { - measurements.unshift(new MetricSegment({ - type: 'template', - value: '$' + variable.name, - expandable: true, - })); - }); + $scope.handleQueryError = function(err) { + $scope.parserError = err.message || 'Failed to issue metric query'; + return []; + }; - return measurements; - }, function(err) { - $scope.parserError = err.message || 'Failed to issue metric query'; - return []; + $scope.transformToSegments = function(results) { + return _.map(results, function(segment) { + return new MetricSegment({ value: segment.text, expandable: segment.expandable }); }); }; + $scope.addTemplateVariableSegments = function(segments) { + _.each(templateSrv.variables, function(variable) { + segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true })); + }); + return segments; + }; + + $scope.getTagsOrValues = function(segment, index) { + var query, queryType; + if (segment.type === 'key' || segment.type === 'plus-button') { + queryType = 'TAG_KEYS'; + query = 'SHOW TAG KEYS FROM "' + $scope.target.measurement + '"'; + } else if (segment.type === 'value') { + queryType = 'TAG_VALUES'; + query = 'SHOW TAG VALUES FROM "' + $scope.target.measurement + '" WITH KEY = ' + $scope.tagSegments[index-2].value; + } else if (segment.type === 'condition') { + return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]); + } + else { + return $q.when([]); + } + + return $scope.datasource.metricFindQuery(query, queryType) + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(function(results) { + if (queryType === 'TAG_KEYS' && segment.type !== 'plus-button') { + results.push(new MetricSegment({fake: true, value: 'remove tag filter'})); + } + return results; + }) + .then(null, $scope.handleQueryError); + }; + + $scope.tagSegmentUpdated = function(segment, index) { + $scope.tagSegments[index] = segment; + + if (segment.value === 'remove tag filter') { + $scope.tagSegments.splice(index, 3); + if ($scope.tagSegments.length === 0) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } else { + $scope.tagSegments.splice(index-1, 1); + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + else { + if (segment.type === 'plus-button') { + if (index > 2) { + $scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND')); + } + $scope.tagSegments.push(new MetricSegment({fake: true, value: '=', type: 'operator'})); + $scope.tagSegments.push(new MetricSegment({fake: true, value: 'select tag value', type: 'value' })); + segment.type = 'key'; + } + + if ((index+1) === $scope.tagSegments.length) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + + $scope.rebuildTargetTagConditions(); + }; + + $scope.rebuildTargetTagConditions = function() { + var tags = [{}]; + var tagIndex = 0; + _.each($scope.tagSegments, function(segment2) { + if (segment2.type === 'key') { + tags[tagIndex].key = segment2.value; + } + else if (segment2.type === 'value') { + tags[tagIndex].value = segment2.value; + } + else if (segment2.type === 'condition') { + tags.push({ condition: segment2.value }); + tagIndex += 1; + } + }); + + $scope.target.tags = tags; + $scope.$parent.get_data(); + }; + function MetricSegment(options) { if (options === '*' || options.value === '*') { this.value = '*'; @@ -107,19 +198,25 @@ function (angular, _) { return; } + this.cssClass = options.cssClass; + this.type = options.type; this.fake = options.fake; this.value = options.value; this.type = options.type; this.expandable = options.expandable; - this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); + this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } MetricSegment.newSelectMeasurement = function() { return new MetricSegment({value: 'select measurement', fake: true}); }; - MetricSegment.newSelectTag = function() { - return new MetricSegment({value: 'select tag', fake: true}); + MetricSegment.newCondition = function(condition) { + return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' }); + }; + + MetricSegment.newPlusButton = function() { + return new MetricSegment({fake: true, html: '', type: 'plus-button' }); }; MetricSegment.newSelectTagValue = function() { diff --git a/public/test/specs/influx09-querybuilder-specs.js b/public/test/specs/influx09-querybuilder-specs.js index 09fb343171a..bec2035760f 100644 --- a/public/test/specs/influx09-querybuilder-specs.js +++ b/public/test/specs/influx09-querybuilder-specs.js @@ -21,7 +21,7 @@ define([ describe('series with tags only', function() { var builder = new InfluxQueryBuilder({ measurement: 'cpu', - tags: {'hostname': 'server1'} + tags: [{key: 'hostname', value: 'server1'}] }); var query = builder.build(); diff --git a/public/test/specs/influxdbQueryCtrl-specs.js b/public/test/specs/influxdbQueryCtrl-specs.js new file mode 100644 index 00000000000..0e177a6aebe --- /dev/null +++ b/public/test/specs/influxdbQueryCtrl-specs.js @@ -0,0 +1,120 @@ +define([ + 'helpers', + 'plugins/datasource/influxdb/queryCtrl' +], function(helpers) { + 'use strict'; + + describe('InfluxDBQueryCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + beforeEach(module('grafana.controllers')); + beforeEach(ctx.providePhase()); + beforeEach(ctx.createControllerPhase('InfluxQueryCtrl')); + + beforeEach(function() { + ctx.scope.target = {}; + ctx.scope.$parent = { get_data: sinon.spy() }; + + ctx.scope.datasource = ctx.datasource; + ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); + }); + + describe('init', function() { + beforeEach(function() { + ctx.scope.init(); + }); + + it('should init tagSegments', function() { + expect(ctx.scope.tagSegments.length).to.be(1); + }); + + it('should init measurementSegment', function() { + expect(ctx.scope.measurementSegment.value).to.be('select measurement'); + }); + }); + + describe('when first tag segment is updated', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0); + }); + + it('should update tag key', function() { + expect(ctx.scope.target.tags[0].key).to.be('asd'); + expect(ctx.scope.tagSegments[0].type).to.be('key'); + }); + + it('should add tagSegments', function() { + expect(ctx.scope.tagSegments.length).to.be(3); + }); + }); + + describe('when last tag value segment is updated', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + }); + + it('should update tag value', function() { + expect(ctx.scope.target.tags[0].value).to.be('server1'); + }); + + it('should add plus button for another filter', function() { + expect(ctx.scope.tagSegments[3].fake).to.be(true); + }); + }); + + describe('when second tag key is added', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + }); + + it('should update tag key', function() { + expect(ctx.scope.target.tags[1].key).to.be('key2'); + }); + + it('should add AND segment', function() { + expect(ctx.scope.tagSegments[3].value).to.be('AND'); + }); + }); + + describe('when condition is changed', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3); + }); + + it('should update tag condition', function() { + expect(ctx.scope.target.tags[1].condition).to.be('OR'); + }); + + it('should update AND segment', function() { + expect(ctx.scope.tagSegments[3].value).to.be('OR'); + expect(ctx.scope.tagSegments.length).to.be(7); + }); + }); + + describe('when deleting is changed', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + ctx.scope.tagSegmentUpdated({value: 'remove tag filter', type: 'key'}, 4); + }); + + it('should remove all segment after 2 and replace with plus button', function() { + expect(ctx.scope.tagSegments.length).to.be(4); + expect(ctx.scope.tagSegments[3].type).to.be('plus-button'); + }); + }); + + }); +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 3e944c3c36f..86196069265 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -128,6 +128,7 @@ require([ 'specs/influxQueryBuilder-specs', 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', + 'specs/influxdbQueryCtrl-specs', 'specs/graph-ctrl-specs', 'specs/graph-specs', 'specs/graph-tooltip-specs', From 09b0e6e388dc802b090ce016838bf0ef4ccb4874 Mon Sep 17 00:00:00 2001 From: "Haneysmith, Nathan" Date: Fri, 15 May 2015 11:11:02 -0700 Subject: [PATCH 176/398] Addresses #1853, redact session provider secrets In cases where a database is used for session storage, redact the session_provider config value. I assumed "@" as the marker for a database vs file/memory. --- pkg/api/admin_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go index 21615219acd..71ed229bba7 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin_settings.go @@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) { for _, key := range section.Keys() { keyName := key.Name() value := key.Value() - if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { value = "************" } From 2af28b90c2a3e9c4fbd5280af90eae490bf1b591 Mon Sep 17 00:00:00 2001 From: "Haneysmith, Nathan" Date: Fri, 15 May 2015 13:25:41 -0700 Subject: [PATCH 177/398] whitespace update per gofmt --- pkg/api/admin_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go index 71ed229bba7..06413d6a0b1 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin_settings.go @@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) { for _, key := range section.Keys() { keyName := key.Name() value := key.Value() - if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { value = "************" } From 6fd37779b8538981dde8fe9d147d285819930209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 19:04:49 +0200 Subject: [PATCH 178/398] More work on new influxdb query editor, #1525 --- .../influxdb/partials/query.editor.html | 2 +- .../datasource/influxdb/queryBuilder.js | 16 ++-- .../plugins/datasource/influxdb/queryCtrl.js | 79 +++++++++++++---- public/css/less/grafana.less | 12 +++ .../test/specs/influx09-querybuilder-specs.js | 35 +++++++- public/test/specs/influxdbQueryCtrl-specs.js | 86 ++++++++++++++++++- 6 files changed, 203 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 494eaff5520..8ea612b0924 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -91,7 +91,7 @@ time($interval)
  • - +
  • + diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index 828ab3809b0..a6d530fe68f 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -266,7 +266,7 @@ function (angular, _, InfluxQueryBuilder) { }; MetricSegment.newPlusButton = function() { - return new MetricSegment({fake: true, html: '', type: 'plus-button' }); + return new MetricSegment({fake: true, html: '', type: 'plus-button' }); }; MetricSegment.newSelectTagValue = function() { diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index 72bcfb547fa..41630400cf7 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -20,6 +20,14 @@ } } +.tight-form-container-no-item-borders { + border: 1px solid @grafanaTargetBorder; + + .tight-form, .tight-form-item { + border: none; + } +} + .spaced-form { .tight-form { margin: 7px 0; @@ -85,9 +93,9 @@ &.last { border-right: none; } - } + .tight-form-item-icon { i { width: 15px; From f41c31432974a54258e6f197dd6817d06d3a3a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 11:47:12 +0200 Subject: [PATCH 186/398] Minor layout fix to templating editor, making input fields a little less wide --- public/app/features/templating/partials/editor.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 0ead15dbb99..11769e34dfc 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -65,7 +65,7 @@ Name
  • - +
  • Type @@ -139,7 +139,7 @@ Query
  • - +
  • @@ -151,7 +151,7 @@ Optional, if you want to extract part of a series name or metric node segment
  • - +
  • @@ -163,7 +163,7 @@
  • - +
  • All format From 5896903bd37739ca4b04186cc18a1982ad27e1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 15:01:05 +0200 Subject: [PATCH 187/398] Began work on alias support and alias patterns for InfluxDB 0.9, #1525 --- .../plugins/datasource/influxdb/datasource.js | 15 +- .../datasource/influxdb/influxSeries.js | 19 +- .../influxdb/partials/query.editor.html | 89 +++++++ public/css/less/tightform.less | 2 +- public/test/specs/influxSeries-specs.js | 245 +++--------------- public/test/specs/influxSeries08-specs.js | 220 ++++++++++++++++ public/test/test-main.js | 1 + 7 files changed, 373 insertions(+), 218 deletions(-) create mode 100644 public/test/specs/influxSeries08-specs.js diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 56b01772c55..de7379d6367 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -69,8 +69,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { var query = annotation.query.replace('$timeFilter', timeFilter); query = templateSrv.replace(query); - return this._seriesQuery(query).then(function(results) { - return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations(); + return this._seriesQuery(query).then(function(data) { + if (!data || !data.results || !data.results[0]) { + throw { message: 'No results in response from InfluxDB' }; + } + return new InfluxSeries({ series: data.results[0].series, annotation: annotation }).getAnnotations(); }); }; @@ -168,9 +171,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return deferred.promise; }; - function handleInfluxQueryResponse(alias, seriesList) { - var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias }); - return influxSeries.getTimeSeries(); + function handleInfluxQueryResponse(alias, data) { + if (!data || !data.results || !data.results[0]) { + throw { message: 'No results in response from InfluxDB' }; + } + return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries(); } function getTimeFilter(options) { diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index cca01459fc4..45e4325c016 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -5,8 +5,7 @@ function (_) { 'use strict'; function InfluxSeries(options) { - this.seriesList = options.seriesList && options.seriesList.results && options.seriesList.results.length > 0 - ? options.seriesList.results[0].series || [] : []; + this.series = options.series; this.alias = options.alias; this.annotation = options.annotation; } @@ -17,23 +16,25 @@ function (_) { var output = []; var self = this; - console.log(self.seriesList); - if (self.seriesList.length === 0) { + if (self.series.length === 0) { return output; } - _.each(self.seriesList, function(series) { + _.each(self.series, function(series) { var datapoints = []; for (var i = 0; i < series.values.length; i++) { datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()]; } var seriesName = series.name; - var tags = _.map(series.tags, function(value, key) { - return key + ': ' + value; - }); - if (tags.length > 0) { + if (self.alias) { + seriesName = self.alias; + } else if (series.tags) { + var tags = _.map(series.tags, function(value, key) { + return key + ': ' + value; + }); + seriesName = seriesName + ' {' + tags.join(', ') + '}'; } diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0881921078d..9a9fca802ea 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -141,14 +141,103 @@
  • +
      +
    • + Alias pattern +
    • +
    • + +
    • +
    +
    +
    +
      +
    • + +
    • +
    • + Group by time interval +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    + + +
    +
    + +
    +
    Alias patterns
    +
      +
    • $m = replaced with measurement name
    • +
    • $measurement = replaced with measurement name
    • +
    • $tag_hostname = replaced with the value of the hostname tag
    • +
    • You can also use [[tag_hostname]] pattern replacement syntax
    • +
    +
    + +
    +
    Stacking and fill
    +
      +
    • When stacking is enabled it important that points align
    • +
    • If there are missing points for one series it can cause gaps or missing bars
    • +
    • You must use fill(0), and select a group by time low limit
    • +
    • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
    • +
    • This will insert zeros for series that are missing measurements and will make stacking work properly
    • +
    +
    + +
    +
    Group by time
    +
      +
    • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
    • +
    • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
    • +
    • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
    • +
    • The low limit can only be set in the group by time option below your queries
    • +
    • You set a low limit by adding a greater sign before the interval
    • +
    • Example: >60s if you write metrics to InfluxDB every 60 seconds
    • +
    +
    + +
    + diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index 41630400cf7..5bd9cda8f43 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -23,7 +23,7 @@ .tight-form-container-no-item-borders { border: 1px solid @grafanaTargetBorder; - .tight-form, .tight-form-item { + .tight-form, .tight-form-item, [type=text].tight-form-input { border: none; } } diff --git a/public/test/specs/influxSeries-specs.js b/public/test/specs/influxSeries-specs.js index 47fb77b67b3..b68a3a95e8c 100644 --- a/public/test/specs/influxSeries-specs.js +++ b/public/test/specs/influxSeries-specs.js @@ -1,218 +1,57 @@ define([ - 'plugins/datasource/influxdb_08/influxSeries' + 'plugins/datasource/influxdb/influxSeries' ], function(InfluxSeries) { 'use strict'; describe('when generating timeseries from influxdb response', function() { describe('given two series', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - }, - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server2.cpu', - points: [[1402596000, 15, 1], [1402596001, 16, 2]] - } - ] - }); - - var result = series.getTimeSeries(); - - it('should generate two time series', function() { - expect(result.length).to.be(2); - expect(result[0].target).to.be('prod.server1.cpu.mean'); - expect(result[0].datapoints[0][0]).to.be(10); - expect(result[0].datapoints[0][1]).to.be(1402596000); - expect(result[0].datapoints[1][0]).to.be(12); - expect(result[0].datapoints[1][1]).to.be(1402596001); - - expect(result[1].target).to.be('prod.server2.cpu.mean'); - expect(result[1].datapoints[0][0]).to.be(15); - expect(result[1].datapoints[0][1]).to.be(1402596000); - expect(result[1].datapoints[1][0]).to.be(16); - expect(result[1].datapoints[1][1]).to.be(1402596001); - }); - - }); - - describe('given an alias format', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$s.testing' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('prod.server1.cpu.testing'); - }); - - }); - - describe('given an alias format with segment numbers', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$1.mean' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('server1.mean'); - }); - - }); - - describe('given an alias format and many segments', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$5.$11.mean' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('a5.a11.mean'); - }); - - }); - - - describe('given an alias format with group by field', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'host'], - name: 'prod.cpu', - points: [[1402596000, 10, 'A']] - } - ], - groupByField: 'host', - alias: '$g.$1' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('A.cpu'); - }); - - }); - - describe('given group by column', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'host'], - name: 'prod.cpu', - points: [ - [1402596000, 10, 'A'], - [1402596001, 11, 'A'], - [1402596000, 5, 'B'], - [1402596001, 6, 'B'], - ] - } - ], - groupByField: 'host' - }); - - var result = series.getTimeSeries(); - - it('should generate two time series', function() { - expect(result.length).to.be(2); - expect(result[0].target).to.be('prod.cpu.A'); - expect(result[0].datapoints[0][0]).to.be(10); - expect(result[0].datapoints[0][1]).to.be(1402596000); - expect(result[0].datapoints[1][0]).to.be(11); - expect(result[0].datapoints[1][1]).to.be(1402596001); - - expect(result[1].target).to.be('prod.cpu.B'); - expect(result[1].datapoints[0][0]).to.be(5); - expect(result[1].datapoints[0][1]).to.be(1402596000); - expect(result[1].datapoints[1][0]).to.be(6); - expect(result[1].datapoints[1][1]).to.be(1402596001); - }); - - }); - - }); - - describe("when creating annotations from influxdb response", function() { - describe('given column mapping for all columns', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'text', 'sequence_number', 'title', 'tags'], - name: 'events1', - points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']] - } - ], - annotation: { - query: 'select', - titleColumn: 'title', - tagsColumn: 'tags', - textColumn: 'text', + var options = { series: [ + { + name: 'cpu', + tags: {app: 'test'}, + columns: ['time', 'mean'], + values: [["2015-05-18T10:57:05Z", 10], ["2015-05-18T10:57:06Z", 12]] + }, + { + name: 'cpu', + tags: {app: 'test2'}, + columns: ['time', 'mean'], + values: [["2015-05-18T10:57:05Z", 15], ["2015-05-18T10:57:06Z", 16]] } + ]}; + + describe('and no alias', function() { + + it('should generate two time series', function() { + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result.length).to.be(2); + expect(result[0].target).to.be('cpu {app: test}'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1431946625000); + expect(result[0].datapoints[1][0]).to.be(12); + expect(result[0].datapoints[1][1]).to.be(1431946626000); + + expect(result[1].target).to.be('cpu {app: test2}'); + expect(result[1].datapoints[0][0]).to.be(15); + expect(result[1].datapoints[0][1]).to.be(1431946625000); + expect(result[1].datapoints[1][0]).to.be(16); + expect(result[1].datapoints[1][1]).to.be(1431946626000); + }); }); - var result = series.getAnnotations(); + describe('and simple alias', function() { + it('should use alias', function() { + options.alias = 'new series'; + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result[0].target).to.be('new series'); + }); - it(' should generate 2 annnotations ', function() { - expect(result.length).to.be(2); - expect(result[0].annotation.query).to.be('select'); - expect(result[0].title).to.be('Hello'); - expect(result[0].time).to.be(1402596000000); - expect(result[0].tags).to.be('B'); - expect(result[0].text).to.be('some text'); }); - - }); - - describe('given no column mapping', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'text', 'sequence_number'], - name: 'events1', - points: [[1402596000000, 'some text', 1]] - } - ], - annotation: { query: 'select' } - }); - - var result = series.getAnnotations(); - - it('should generate 1 annnotation', function() { - expect(result.length).to.be(1); - expect(result[0].title).to.be('some text'); - expect(result[0].time).to.be(1402596000000); - expect(result[0].tags).to.be(undefined); - expect(result[0].text).to.be(undefined); - }); - }); }); diff --git a/public/test/specs/influxSeries08-specs.js b/public/test/specs/influxSeries08-specs.js new file mode 100644 index 00000000000..47fb77b67b3 --- /dev/null +++ b/public/test/specs/influxSeries08-specs.js @@ -0,0 +1,220 @@ +define([ + 'plugins/datasource/influxdb_08/influxSeries' +], function(InfluxSeries) { + 'use strict'; + + describe('when generating timeseries from influxdb response', function() { + + describe('given two series', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + }, + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server2.cpu', + points: [[1402596000, 15, 1], [1402596001, 16, 2]] + } + ] + }); + + var result = series.getTimeSeries(); + + it('should generate two time series', function() { + expect(result.length).to.be(2); + expect(result[0].target).to.be('prod.server1.cpu.mean'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1402596000); + expect(result[0].datapoints[1][0]).to.be(12); + expect(result[0].datapoints[1][1]).to.be(1402596001); + + expect(result[1].target).to.be('prod.server2.cpu.mean'); + expect(result[1].datapoints[0][0]).to.be(15); + expect(result[1].datapoints[0][1]).to.be(1402596000); + expect(result[1].datapoints[1][0]).to.be(16); + expect(result[1].datapoints[1][1]).to.be(1402596001); + }); + + }); + + describe('given an alias format', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$s.testing' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('prod.server1.cpu.testing'); + }); + + }); + + describe('given an alias format with segment numbers', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$1.mean' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('server1.mean'); + }); + + }); + + describe('given an alias format and many segments', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$5.$11.mean' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('a5.a11.mean'); + }); + + }); + + + describe('given an alias format with group by field', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'host'], + name: 'prod.cpu', + points: [[1402596000, 10, 'A']] + } + ], + groupByField: 'host', + alias: '$g.$1' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('A.cpu'); + }); + + }); + + describe('given group by column', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'host'], + name: 'prod.cpu', + points: [ + [1402596000, 10, 'A'], + [1402596001, 11, 'A'], + [1402596000, 5, 'B'], + [1402596001, 6, 'B'], + ] + } + ], + groupByField: 'host' + }); + + var result = series.getTimeSeries(); + + it('should generate two time series', function() { + expect(result.length).to.be(2); + expect(result[0].target).to.be('prod.cpu.A'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1402596000); + expect(result[0].datapoints[1][0]).to.be(11); + expect(result[0].datapoints[1][1]).to.be(1402596001); + + expect(result[1].target).to.be('prod.cpu.B'); + expect(result[1].datapoints[0][0]).to.be(5); + expect(result[1].datapoints[0][1]).to.be(1402596000); + expect(result[1].datapoints[1][0]).to.be(6); + expect(result[1].datapoints[1][1]).to.be(1402596001); + }); + + }); + + }); + + describe("when creating annotations from influxdb response", function() { + describe('given column mapping for all columns', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'text', 'sequence_number', 'title', 'tags'], + name: 'events1', + points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']] + } + ], + annotation: { + query: 'select', + titleColumn: 'title', + tagsColumn: 'tags', + textColumn: 'text', + } + }); + + var result = series.getAnnotations(); + + it(' should generate 2 annnotations ', function() { + expect(result.length).to.be(2); + expect(result[0].annotation.query).to.be('select'); + expect(result[0].title).to.be('Hello'); + expect(result[0].time).to.be(1402596000000); + expect(result[0].tags).to.be('B'); + expect(result[0].text).to.be('some text'); + }); + + }); + + describe('given no column mapping', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'text', 'sequence_number'], + name: 'events1', + points: [[1402596000000, 'some text', 1]] + } + ], + annotation: { query: 'select' } + }); + + var result = series.getAnnotations(); + + it('should generate 1 annnotation', function() { + expect(result.length).to.be(1); + expect(result[0].title).to.be('some text'); + expect(result[0].time).to.be(1402596000000); + expect(result[0].tags).to.be(undefined); + expect(result[0].text).to.be(undefined); + }); + + }); + + }); + +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 86196069265..f064b81bab2 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -125,6 +125,7 @@ require([ 'specs/graphiteTargetCtrl-specs', 'specs/graphiteDatasource-specs', 'specs/influxSeries-specs', + 'specs/influxSeries08-specs', 'specs/influxQueryBuilder-specs', 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', From e9f38b9fc0d27c5f1ad4945fed19fd9e5b426f19 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Mon, 18 May 2015 10:01:58 -0400 Subject: [PATCH 188/398] no unbound recursion in publish() unbound recursion approach can blow up call stack, and - I think - allocate memory unboundedly as well. We can simply loop until err != nil I didn't actually test this live, though tests succeed --- pkg/services/eventpublisher/eventpublisher.go | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/services/eventpublisher/eventpublisher.go b/pkg/services/eventpublisher/eventpublisher.go index 14e527b2cc7..2854b63a9a5 100644 --- a/pkg/services/eventpublisher/eventpublisher.go +++ b/pkg/services/eventpublisher/eventpublisher.go @@ -109,25 +109,26 @@ func Setup() error { } func publish(routingKey string, msgString []byte) { - err := channel.Publish( - exchange, //exchange - routingKey, // routing key - false, // mandatory - false, // immediate - amqp.Publishing{ - ContentType: "application/json", - Body: msgString, - }, - ) - if err != nil { + for { + err := channel.Publish( + exchange, //exchange + routingKey, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: msgString, + }, + ) + if err == nil { + return + } // failures are most likely because the connection was lost. // the connection will be re-established, so just keep // retrying every 2seconds until we successfully publish. time.Sleep(2 * time.Second) fmt.Println("publish failed, retrying.") - publish(routingKey, msgString) } - return } func eventListener(event interface{}) error { From 5270c4bc740371468b0af536a20d28935eb72fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 17:28:15 +0200 Subject: [PATCH 189/398] refactorin api code for user routes, preparation for admin improvements, #2014 --- pkg/api/api.go | 12 +- pkg/api/common.go | 111 ++++++++++++++++++ pkg/api/index.go | 2 +- pkg/api/user.go | 45 ++++--- pkg/models/org.go | 7 +- .../features/admin/partials/edit_user.html | 4 +- .../app/features/admin/partials/new_user.html | 6 +- .../features/profile/partials/profile.html | 4 +- 8 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 pkg/api/common.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 14e693752b9..cfc1f50b98e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -55,15 +55,21 @@ func Register(r *macaron.Macaron) { r.Group("/api", func() { // user r.Group("/user", func() { - r.Get("/", GetUser) + r.Get("/", wrap(GetSignedInUser)) r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) r.Post("/using/:id", UserSetUsingOrg) - r.Get("/orgs", GetUserOrgList) + r.Get("/orgs", wrap(GetSignedInUserOrgList)) r.Post("/stars/dashboard/:id", StarDashboard) r.Delete("/stars/dashboard/:id", UnstarDashboard) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) }) + // users + r.Group("/users", func() { + r.Get("/:id/", wrap(GetUserById)) + r.Get("/:id/org", wrap(GetUserOrgList)) + }, reqGrafanaAdmin) + // account r.Group("/org", func() { r.Get("/", GetOrg) @@ -127,5 +133,5 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) - r.NotFound(NotFound) + r.NotFound(NotFoundHandler) } diff --git a/pkg/api/common.go b/pkg/api/common.go new file mode 100644 index 00000000000..8757318159e --- /dev/null +++ b/pkg/api/common.go @@ -0,0 +1,111 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/Unknwon/macaron" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + NotFound = ApiError(404, "Not found", nil) + ServerError = ApiError(500, "Server error", nil) +) + +type Response interface { + WriteTo(out http.ResponseWriter) +} + +type NormalResponse struct { + status int + body []byte + header http.Header +} + +func wrap(action func(c *middleware.Context) Response) macaron.Handler { + return func(c *middleware.Context) { + res := action(c) + if res == nil { + res = ServerError + } + res.WriteTo(c.Resp) + } +} + +func (r *NormalResponse) WriteTo(out http.ResponseWriter) { + header := out.Header() + for k, v := range r.header { + header[k] = v + } + out.WriteHeader(r.status) + out.Write(r.body) +} + +func (r *NormalResponse) Cache(ttl string) *NormalResponse { + return r.Header("Cache-Control", "public,max-age="+ttl) +} + +func (r *NormalResponse) Header(key, value string) *NormalResponse { + r.header.Set(key, value) + return r +} + +// functions to create responses + +func Empty(status int) *NormalResponse { + return Respond(status, nil) +} + +func Json(status int, body interface{}) *NormalResponse { + return Respond(status, body).Header("Content-Type", "application/json") +} + +func ApiError(status int, message string, err error) *NormalResponse { + resp := make(map[string]interface{}) + + if err != nil { + log.Error(4, "%s: %v", message, err) + if setting.Env != setting.PROD { + resp["error"] = err.Error() + } + } + + switch status { + case 404: + resp["message"] = "Not Found" + metrics.M_Api_Status_500.Inc(1) + case 500: + metrics.M_Api_Status_404.Inc(1) + resp["message"] = "Internal Server Error" + } + + if message != "" { + resp["message"] = message + } + + return Json(status, resp) +} + +func Respond(status int, body interface{}) *NormalResponse { + var b []byte + var err error + switch t := body.(type) { + case []byte: + b = t + case string: + b = []byte(t) + default: + if b, err = json.Marshal(body); err != nil { + return ApiError(500, "body json marshal", err) + } + } + return &NormalResponse{ + body: b, + status: status, + header: make(http.Header), + } +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 386bb3351df..8f486c4b785 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -59,7 +59,7 @@ func Index(c *middleware.Context) { c.HTML(200, "index") } -func NotFound(c *middleware.Context) { +func NotFoundHandler(c *middleware.Context) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return diff --git a/pkg/api/user.go b/pkg/api/user.go index 9d870a10a1b..e7cc8ff0366 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -7,15 +7,24 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func GetUser(c *middleware.Context) { - query := m.GetUserProfileQuery{UserId: c.UserId} +// GET /api/user (current authenticated user) +func GetSignedInUser(c *middleware.Context) Response { + return getUserUserProfile(c.UserId) +} + +// GET /api/user/:id +func GetUserById(c *middleware.Context) Response { + return getUserUserProfile(c.ParamsInt64(":id")) +} + +func getUserUserProfile(userId int64) Response { + query := m.GetUserProfileQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get user", err) - return + return ApiError(500, "Failed to get user", err) } - c.JSON(200, query.Result) + return Json(200, query.Result) } func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { @@ -29,22 +38,24 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { c.JsonOK("User updated") } -func GetUserOrgList(c *middleware.Context) { - query := m.GetUserOrgListQuery{UserId: c.UserId} +// GET /api/user/orgs +func GetSignedInUserOrgList(c *middleware.Context) Response { + return getUserOrgList(c.UserId) +} + +// GET /api/user/:id/orgs +func GetUserOrgList(c *middleware.Context) Response { + return getUserOrgList(c.ParamsInt64(":id")) +} + +func getUserOrgList(userId int64) Response { + query := m.GetUserOrgListQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get user organizations", err) - return + return ApiError(500, "Faile to get user organziations", err) } - for _, ac := range query.Result { - if ac.OrgId == c.OrgId { - ac.IsUsing = true - break - } - } - - c.JSON(200, query.Result) + return Json(200, query.Result) } func validateUsingOrg(userId int64, orgId int64) bool { diff --git a/pkg/models/org.go b/pkg/models/org.go index ab6d97b9ae8..a8ee08ea69b 100644 --- a/pkg/models/org.go +++ b/pkg/models/org.go @@ -58,8 +58,7 @@ type OrgDTO struct { } type UserOrgDTO struct { - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Role RoleType `json:"role"` - IsUsing bool `json:"isUsing"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Role RoleType `json:"role"` } diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 9b2a18fd010..c82d7705b8d 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -25,7 +25,7 @@
    -
    +
    • Email @@ -36,7 +36,7 @@
    -
    +
    • Username diff --git a/public/app/features/admin/partials/new_user.html b/public/app/features/admin/partials/new_user.html index 48f78fb76b6..73877f9bda4 100644 --- a/public/app/features/admin/partials/new_user.html +++ b/public/app/features/admin/partials/new_user.html @@ -24,7 +24,7 @@
    -
    +
    • Email @@ -35,7 +35,7 @@
    -
    +
    • Username @@ -46,7 +46,7 @@
    -
    +
    • Password diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index d8758ae9c09..5512245b275 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -71,10 +71,10 @@
    Name: {{org.name}} Role: {{org.role}} - + Current - + Select
    + + + + + +
    Name: {{org.name}}Role: {{org.role}} + + Current + +
    +
  • From 788e7fd36d09dfa5487e536d715d733be33bb82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 10:16:32 +0200 Subject: [PATCH 196/398] Lots of api refactoring for org routes, #2014 --- pkg/api/api.go | 35 ++++-- pkg/api/org.go | 50 ++++++--- pkg/api/org_users.go | 105 ++++++++++++------ pkg/models/user.go | 1 + pkg/services/sqlstore/user.go | 1 + .../features/admin/partials/edit_user.html | 25 +++-- public/app/features/admin/partials/users.html | 2 +- public/app/features/org/newOrgCtrl.js | 2 +- .../app/features/org/partials/orgUsers.html | 2 +- .../datasource/influxdb/queryBuilder.js | 2 - .../test/specs/influx09-querybuilder-specs.js | 15 ++- 11 files changed, 155 insertions(+), 85 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index f45da26d9c2..1c63ad3a541 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) { reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}) reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) - reqAccountAdmin := middleware.RoleAuth(m.ROLE_ADMIN) + regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) bind := binding.Bind // not logged in views @@ -71,23 +71,34 @@ func Register(r *macaron.Macaron) { r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) - // account + // current org r.Group("/org", func() { - r.Get("/", GetOrg) - r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg) - r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg) - r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser) - r.Get("/users", GetOrgUsers) - r.Patch("/users/:id", bind(m.UpdateOrgUserCommand{}), UpdateOrgUser) - r.Delete("/users/:id", RemoveOrgUser) - }, reqAccountAdmin) + r.Get("/", wrap(GetOrgCurrent)) + r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrgCurrent)) + r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg)) + r.Get("/users", wrap(GetOrgUsersForCurrentOrg)) + r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg)) + r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg)) + }, regOrgAdmin) + + // create new org + r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg)) + + // orgs (admin routes) + r.Group("/orgs/:orgId", func() { + r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg)) + r.Get("/users", wrap(GetOrgUsers)) + r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser)) + r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser)) + r.Delete("/users/:userId", wrap(RemoveOrgUser)) + }, reqGrafanaAdmin) // auth api keys r.Group("/auth/keys", func() { r.Get("/", wrap(GetApiKeys)) r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) r.Delete("/:id", wrap(DeleteApiKey)) - }, reqAccountAdmin) + }, regOrgAdmin) // Data sources r.Group("/datasources", func() { @@ -98,7 +109,7 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", DeleteDataSource) r.Get("/:id", GetDataSourceById) r.Get("/plugins", GetDataSourcePlugins) - }, reqAccountAdmin) + }, regOrgAdmin) r.Get("/frontend/settings/", GetFrontendSettings) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) diff --git a/pkg/api/org.go b/pkg/api/org.go index ac8727c9e49..5b9e1a9aef8 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -8,17 +8,25 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func GetOrg(c *middleware.Context) { - query := m.GetOrgByIdQuery{Id: c.OrgId} +// GET /api/org +func GetOrgCurrent(c *middleware.Context) Response { + return getOrgHelper(c.OrgId) +} + +// GET /api/orgs/:orgId +func GetOrgById(c *middleware.Context) Response { + return getOrgHelper(c.ParamsInt64(":orgId")) +} + +func getOrgHelper(orgId int64) Response { + query := m.GetOrgByIdQuery{Id: orgId} if err := bus.Dispatch(&query); err != nil { if err == m.ErrOrgNotFound { - c.JsonApiErr(404, "Organization not found", err) - return + return ApiError(404, "Organization not found", err) } - c.JsonApiErr(500, "Failed to get organization", err) - return + return ApiError(500, "Failed to get organization", err) } org := m.OrgDTO{ @@ -26,33 +34,41 @@ func GetOrg(c *middleware.Context) { Name: query.Result.Name, } - c.JSON(200, &org) + return Json(200, &org) } -func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { +// POST /api/orgs +func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin { - c.JsonApiErr(401, "Access denied", nil) - return + return ApiError(401, "Access denied", nil) } cmd.UserId = c.UserId if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to create organization", err) - return + return ApiError(500, "Failed to create organization", err) } metrics.M_Api_Org_Create.Inc(1) - c.JsonOK("Organization created") + return ApiSuccess("Organization created") } -func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) { +// PUT /api/org +func UpdateOrgCurrent(c *middleware.Context, cmd m.UpdateOrgCommand) Response { cmd.OrgId = c.OrgId + return updateOrgHelper(cmd) +} +// PUT /api/orgs/:orgId +func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + return updateOrgHelper(cmd) +} + +func updateOrgHelper(cmd m.UpdateOrgCommand) Response { if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to update organization", err) - return + return ApiError(500, "Failed to update organization", err) } - c.JsonOK("Organization updated") + return ApiSuccess("Organization updated") } diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 8a372c791b9..c88df600450 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -6,77 +6,112 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) { +// POST /api/org/users +func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response { + cmd.OrgId = c.OrgId + return addOrgUserHelper(cmd) +} + +// POST /api/orgs/:orgId/users +func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + return addOrgUserHelper(cmd) +} + +func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return + return ApiError(400, "Invalid role specified", nil) } userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail} err := bus.Dispatch(&userQuery) if err != nil { - c.JsonApiErr(404, "User not found", nil) - return + return ApiError(404, "User not found", nil) } userToAdd := userQuery.Result - if userToAdd.Id == c.UserId { - c.JsonApiErr(400, "Cannot add yourself as user", nil) - return - } + // if userToAdd.Id == c.UserId { + // return ApiError(400, "Cannot add yourself as user", nil) + // } - cmd.OrgId = c.OrgId cmd.UserId = userToAdd.Id if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Could not add user to organization", err) - return + return ApiError(500, "Could not add user to organization", err) } - c.JsonOK("User added to organization") + return ApiSuccess("User added to organization") } -func GetOrgUsers(c *middleware.Context) { - query := m.GetOrgUsersQuery{OrgId: c.OrgId} +// GET /api/org/users +func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { + return getOrgUsersHelper(c.OrgId) +} + +// GET /api/orgs/:orgId/users +func GetOrgUsers(c *middleware.Context) Response { + return getOrgUsersHelper(c.ParamsInt64(":orgId")) +} + +func getOrgUsersHelper(orgId int64) Response { + query := m.GetOrgUsersQuery{OrgId: orgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get account user", err) - return + return ApiError(500, "Failed to get account user", err) } - c.JSON(200, query.Result) + return Json(200, query.Result) } -func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) { - if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return - } - - cmd.UserId = c.ParamsInt64(":id") +// PATCH /api/org/users/:userId +func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { cmd.OrgId = c.OrgId + cmd.UserId = c.ParamsInt64(":userId") + return updateOrgUserHelper(cmd) +} + +// PATCH /api/orgs/:orgId/users/:userId +func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + cmd.UserId = c.ParamsInt64(":userId") + return updateOrgUserHelper(cmd) +} + +func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { + if !cmd.Role.IsValid() { + return ApiError(400, "Invalid role specified", nil) + } if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed update org user", err) - return + return ApiError(500, "Failed update org user", err) } - c.JsonOK("Organization user updated") + return ApiSuccess("Organization user updated") } -func RemoveOrgUser(c *middleware.Context) { - userId := c.ParamsInt64(":id") +// DELETE /api/org/users/:userId +func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response { + userId := c.ParamsInt64(":userId") + return removeOrgUserHelper(c.OrgId, userId) +} - cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId} +// DELETE /api/orgs/:orgId/users/:userId +func RemoveOrgUser(c *middleware.Context) Response { + userId := c.ParamsInt64(":userId") + orgId := c.ParamsInt64(":orgId") + return removeOrgUserHelper(orgId, userId) +} + +func removeOrgUserHelper(orgId int64, userId int64) Response { + cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrLastOrgAdmin { - c.JsonApiErr(400, "Cannot remove last organization admin", nil) - return + return ApiError(400, "Cannot remove last organization admin", nil) } - c.JsonApiErr(500, "Failed to remove user from organization", err) + return ApiError(500, "Failed to remove user from organization", err) } - c.JsonOK("User removed from organization") + return ApiSuccess("User removed from organization") } diff --git a/pkg/models/user.go b/pkg/models/user.go index 68e0001b99c..5efecc8deef 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -133,6 +133,7 @@ type UserProfileDTO struct { Name string `json:"name"` Login string `json:"login"` Theme string `json:"theme"` + OrgId int64 `json:"orgId"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 9f79081783b..f5df6f9ff1f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -236,6 +236,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error { Login: user.Login, Theme: user.Theme, IsGrafanaAdmin: user.IsAdmin, + OrgId: user.OrgId, } return err diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 712aee26d5d..e72bd5b57c9 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -98,17 +98,26 @@ Organizations - +
    + + + + + - - - + +
    NameRole
    Name: {{org.name}}Role: {{org.role}} - - Current - + + {{org.name}} Current + + + + + +
    -
    diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index 14bcd922b5e..6d4f7a8671c 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -1,4 +1,4 @@ - +
    diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index a0549414b6a..6883410cd01 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -226,6 +226,42 @@
    +
    +
    +
    Value Groups/Tags
    +
    +
      +
    • + Tags query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Tags values query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +
    +
    Preview of values (shows max 20)
    diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 6db5ce595a2..7330be1b5cd 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -120,7 +120,7 @@ function (angular, _, kbn) { } return datasourceSrv.get(variable.datasource).then(function(datasource) { - return datasource.metricFindQuery(variable.query).then(function (results) { + var queryPromise = datasource.metricFindQuery(variable.query).then(function (results) { variable.options = self.metricNamesToVariableValues(variable, results); if (variable.includeAll) { @@ -138,6 +138,19 @@ function (angular, _, kbn) { return self.setVariableValue(variable, variable.options[0]); }); + + if (variable.useTags) { + return queryPromise.then(function() { + datasource.metricFindQuery(variable.tagsQuery).then(function (results) { + variable.tags = []; + for (var i = 0; i < results.length; i++) { + variable.tags.push(results[i].text); + } + }); + }); + } else { + return queryPromise; + } }); }; From 592330b5a74983f9e094ba9f9a18bdaff549e00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 May 2015 18:29:20 +0200 Subject: [PATCH 202/398] Expose data source extended properties (jsonData), to the frontend, Closes #2023 --- pkg/api/frontendsettings.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3af191a7af7..ea154320608 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -54,6 +54,10 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } + if len(ds.JsonData) > 0 { + dsMap["jsonData"] = ds.JsonData + } + if ds.Access == m.DS_ACCESS_DIRECT { if ds.BasicAuth { dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword) From 9bedd83f396f6e4423006d9ded39cd031930c2cc Mon Sep 17 00:00:00 2001 From: Felix Rabe Date: Thu, 21 May 2015 00:13:50 +0200 Subject: [PATCH 203/398] Fix latest.json The trailing comma trips up both Python and JS JSON parsers. --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 4d3c471727e..a85d79df539 100644 --- a/latest.json +++ b/latest.json @@ -1,3 +1,3 @@ { - "version": "2.0.2", + "version": "2.0.2" } From ba3f6f9d3e12882f3df96e320f118253c90b0776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 May 2015 10:20:49 +0200 Subject: [PATCH 204/398] InfluxDB 09 fix, do not treat empty results as an error --- public/app/plugins/datasource/influxdb/datasource.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index ffed5e0c520..2ff0f8beb0e 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -43,7 +43,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { // build query var queryBuilder = new InfluxQueryBuilder(target); var query = queryBuilder.build(); - console.log('query builder result:' + query); // replace grafana variables query = query.replace('$timeFilter', timeFilter); @@ -173,7 +172,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { function handleInfluxQueryResponse(alias, data) { if (!data || !data.results || !data.results[0].series) { - throw { message: 'No results in response from InfluxDB' }; + return []; } return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries(); } From 1821809e69d9339672e9d8a58e702f83a9e384e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20K=C3=B6hler?= Date: Thu, 21 May 2015 12:46:26 +0200 Subject: [PATCH 205/398] listing of all none admin methods --- docs/sources/reference/http_api.md | 204 ++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index c0b63050199..3f5fa6392f9 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -141,12 +141,214 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title. +### Gets the home dashboard + +`GET /api/dashboards/home` + +### Tags for Dashboard + +`GET /api/dashboards/tags` + +### Dashboard from JSON file + +`GET /file/:file` + +### Search Dashboards + +`GET /api/search/` + +Status Codes: + +- **query** – Search Query +- **tags** – Tags to use +- **starred** – Flag indicating if only starred Dashboards should be returned +- **tagcloud** - Flag indicating if a tagcloud should be returned + +**Example Request**: + + GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1 + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + ## Data sources +### Get all datasources + +`GET /api/datasources` + +### Get a single data sources by Id + +`GET /api/datasources/:datasourceId` + ### Create data source -## Organizations +`PUT /api/datasources` + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Datasource added"} + +### Edit an existing data source + +`POST /api/datasources` + +### Delete an existing data source + +`DELETE /api/datasources/:datasourceId` + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Data source deleted"} + +### Available data source types + +`GET /api/datasources/plugins` + +## Data source proxy calls + +`GET /api/datasources/proxy/:datasourceId/*` + +Proxies all calls to the actual datasource. + +## Organisation + +### Get current Organisation + +`GET /api/org` + +### Get all users within the actual organisation + +`GET /api/org/users` + +### Add a new user to the actual organisation + +`POST /api/org/users` + +Adds a global user to the actual organisation. + +### Updates the given user + +`PATCH /api/org/users/:userId` + +### Delete user in actual organisation + +`DELETE /api/org/users/:userId` + +### Get all Users + +`GET /api/org/users` + +## Organisations + +### Search all Organisations + +`GET /api/orgs` + +### Update Organisation + +`PUT /api/orgs/:orgId` + +### Get Users in Organisation + +`GET /api/orgs/:orgId/users` + +### Add User in Organisation + +`POST /api/orgs/:orgId/users` + +### Update Users in Organisation + +`PATCH /api/orgs/:orgId/users/:userId` + +### Delete User in Organisation + +`DELETE /api/orgs/:orgId/users/:userId` ## Users +### Search Users +`GET /api/users` + +### Get single user by Id + +`GET /api/users/:id` + +### User Update + +`PUT /api/users/:id` + +### Get Organisations for user + +`GET /api/users/:id/orgs` + +## User + +### Change Password + +`PUT /api/user/password` + +Changes the password for the user + +### Actual User + +`GET /api/user` + +The above will return the current user. + +### Switch user context + +`POST /api/user/using/:organisationId` + +Switch user context to the given organisation. + +### Organisations of the actual User + +`GET /api/user/orgs` + +The above will return a list of all organisations of the current user. + +### Star a dashboard + +`POST /api/user/stars/dashboard/:dashboardId` + +Stars the given Dashboard for the actual user. + +### Unstar a dashboard + +`DELETE /api/user/stars/dashboard/:dashboardId` + +Deletes the staring of the given Dashboard for the actual user. + +## Snapshots + +### Create new snapshot + +`POST /api/snapshots` + +### Get Snapshot by Id + +`GET /api/snapshots/:key` + +### Delete Snapshot by Id + +`DELETE /api/snapshots-delete/:key` + +## Frontend Settings + +### Get Settings + +`GET /api/frontend/settings` + +## Login + +### Renew session based on remember cookie + +`GET /api/login/ping` \ No newline at end of file From 3c6b647398ac8ba49259316f7965889ea053aa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20K=C3=B6hler?= Date: Thu, 21 May 2015 12:56:30 +0200 Subject: [PATCH 206/398] added admin stuff --- docs/sources/reference/http_api.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index 3f5fa6392f9..d888071bd35 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -351,4 +351,26 @@ Deletes the staring of the given Dashboard for the actual user. ### Renew session based on remember cookie -`GET /api/login/ping` \ No newline at end of file +`GET /api/login/ping` + +## Admin + +### Settings + +`GET /api/admin/settings` + +### Global Users + +`POST /api/admin/users` + +### Password for User + +`PUT /api/admin/users/:id/password` + +### Permissions + +`PUT /api/admin/users/:id/permissions` + +### Delete global User + +`DELETE /api/admin/users/:id` From 2191921690ff89fd1680bbac742133b439d62213 Mon Sep 17 00:00:00 2001 From: David Raifaizen Date: Thu, 21 May 2015 14:36:35 -0400 Subject: [PATCH 207/398] Fixed variable name for annotations --- public/app/plugins/datasource/influxdb/influxSeries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index 45e4325c016..949ef88efeb 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -48,7 +48,7 @@ function (_) { var list = []; var self = this; - _.each(this.seriesList, function (series) { + _.each(this.series, function (series) { var titleCol = null; var timeCol = null; var tagsCol = null; From 3dc2a114fadc57e21bcf17fd0e7468255d34d978 Mon Sep 17 00:00:00 2001 From: robert jakub Date: Fri, 22 May 2015 17:08:10 +0200 Subject: [PATCH 208/398] add pps (packet per second) format --- public/app/components/kbn.js | 2 ++ public/app/panels/graph/graph.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index 15a4c7637c3..c9d2900f53b 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -383,6 +383,7 @@ function($, _, moment) { kbn.valueFormats.mbytes = kbn.formatFuncCreator(1024, [' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); + kbn.valueFormats.pps = kbn.formatFuncCreator(1000, [' pps', ' Kpps', ' Mpps', ' Gpps', ' Tpps', ' Ppps', ' Epps', ' Zpps', ' Ypps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']); kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); @@ -564,6 +565,7 @@ function($, _, moment) { { text: 'data rate', submenu: [ + {text: 'packets/sec', value: 'pps'}, {text: 'bits/sec', value: 'bps'}, {text: 'bytes/sec', value: 'Bps'}, ] diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index e090ce4e85e..c6ca8ec7227 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -480,6 +480,9 @@ function (angular, $, kbn, moment, _, GraphTooltip) { case 'bps': url += '&yUnitSystem=si'; break; + case 'pps': + url += '&yUnitSystem=si'; + break; case 'Bps': url += '&yUnitSystem=si'; break; From b55d9350e7518462e1424fe97f355b6e1409d757 Mon Sep 17 00:00:00 2001 From: Indrek Juhkam Date: Sat, 23 May 2015 17:06:51 +0300 Subject: [PATCH 209/398] Add github organizations support --- conf/defaults.ini | 1 + conf/sample.ini | 3 +- pkg/api/login_oauth.go | 2 + pkg/social/social.go | 135 ++++++++++++++++++++++++++++++++--------- 4 files changed, 112 insertions(+), 29 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 628df5360e4..258a0198155 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -153,6 +153,7 @@ token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user team_ids = allowed_domains = +allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/conf/sample.ini b/conf/sample.ini index df204a7f45d..3c2773fa674 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -146,12 +146,13 @@ ;allow_sign_up = false ;client_id = some_id ;client_secret = some_secret -;scopes = user:email +;scopes = user:email,read:org ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user ;team_ids = ;allowed_domains = +;allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 505c17ddde8..796599df864 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -48,6 +48,8 @@ func OAuthLogin(ctx *middleware.Context) { if err != nil { if err == social.ErrMissingTeamMembership { ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled")) + } else if err == social.ErrMissingOrganizationMembership { + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled")) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } diff --git a/pkg/social/social.go b/pkg/social/social.go index 355f85b54b6..49812ddd87f 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -78,12 +78,14 @@ func NewOAuthService() { if name == "github" { setting.OAuthService.GitHub = true teamIds := sec.Key("team_ids").Ints(",") + allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") SocialMap["github"] = &SocialGithub{ - Config: &config, - allowedDomains: info.AllowedDomains, - apiUrl: info.ApiUrl, - allowSignup: info.AllowSignup, - teamIds: teamIds, + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } } @@ -115,16 +117,21 @@ func isEmailAllowed(email string, allowedDomains []string) bool { type SocialGithub struct { *oauth2.Config - allowedDomains []string - apiUrl string - allowSignup bool - teamIds []int + allowedDomains []string + allowedOrganizations []string + apiUrl string + allowSignup bool + teamIds []int } var ( ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") ) +var ( + ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") +) + func (s *SocialGithub) Type() int { return int(models.GITHUB) } @@ -137,26 +144,100 @@ func (s *SocialGithub) IsSignupAllowed() bool { return s.allowSignup } -func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool { - var data struct { - Url string `json:"url"` - State string `json:"state"` +func (s *SocialGithub) IsTeamMember(client *http.Client) bool { + if len(s.teamIds) == 0 { + return true } - membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username) - r, err := client.Get(membershipUrl) + teamMemberships, err := s.FetchTeamMemberships(client) if err != nil { return false } - defer r.Body.Close() + for _, teamId := range s.teamIds { + for _, membershipId := range teamMemberships { + if teamId == membershipId { + return true + } + } + } - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return false +} + +func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { return false } - active := data.State == "active" - return active + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { + type Record struct { + Id int `json:"id"` + } + + membershipUrl := fmt.Sprintf("https://api.github.com/user/teams") + r, err := client.Get(membershipUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var ids = make([]int, len(records)) + for i, record := range records { + ids[i] = record.Id + } + + return ids, nil +} + +func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf("https://api.github.com/user/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil } func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { @@ -185,17 +266,15 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { Email: data.Email, } - if len(s.teamIds) > 0 { - for _, teamId := range s.teamIds { - if s.IsTeamMember(client, data.Name, teamId) { - return userInfo, nil - } - } - + if !s.IsTeamMember(client) { return nil, ErrMissingTeamMembership - } else { - return userInfo, nil } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + return userInfo, nil } // ________ .__ From b2a0ae0f83e1c3d98a8f7822fabefac86b0230ab Mon Sep 17 00:00:00 2001 From: Brandon Turner Date: Mon, 25 May 2015 01:50:29 -0500 Subject: [PATCH 210/398] Render panel images with any SSL protocol This uses any available SSL protocol (instead the phantomjs default: SSLv3) to render panels to PNGs. This is useful when reverse proxing grafana and SSLv3 is disabled due to security vulnerabilities or other reasons. --- pkg/components/renderer/renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index aa9e0c92525..9d5ddd00d73 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -26,7 +26,7 @@ func RenderToPng(params *RenderOpts) (string, error) { pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath = pngPath + ".png" - cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, + cmd := exec.Command(binPath, "--ignore-ssl-errors=true", "--ssl-protocol=any", scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, "domain="+setting.Domain, "sessionid="+params.SessionId) stdout, err := cmd.StdoutPipe() From 0047ce067dc4a194e2ac475f624cdfb4c64963e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 May 2015 10:23:32 +0200 Subject: [PATCH 211/398] SingleStatPanel: fix for color thresholds and value to text mapping combo, Fixes #2044 --- public/app/panels/singlestat/module.js | 13 +++++++------ public/app/partials/login.html | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/public/app/panels/singlestat/module.js b/public/app/panels/singlestat/module.js index 3c698bfd553..24855634ba6 100644 --- a/public/app/panels/singlestat/module.js +++ b/public/app/panels/singlestat/module.js @@ -190,7 +190,12 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { data.flotpairs = $scope.series[0].flotpairs; } - // first check value to text mappings + var decimalInfo = $scope.getDecimalsForValue(data.value); + var formatFunc = kbn.valueFormats[$scope.panel.format]; + data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + + // check value to text mappings for(var i = 0; i < $scope.panel.valueMaps.length; i++) { var map = $scope.panel.valueMaps[i]; // special null case @@ -201,6 +206,7 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { } continue; } + // value/number to text mapping var value = parseFloat(map.value); if (value === data.value) { @@ -212,11 +218,6 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { if (data.value === null || data.value === void 0) { data.valueFormated = "no value"; } - - var decimalInfo = $scope.getDecimalsForValue(data.value); - var formatFunc = kbn.valueFormats[$scope.panel.format]; - data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); - data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); }; $scope.removeValueMap = function(map) { diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8db858dac8d..437588dd652 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -19,7 +19,7 @@
    -
    -
    -
    Value Groups/Tags
    -
    -
      -
    • - Tags query -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Tags values query -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - -
    • -
    -
    -
    -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    From 494ede5bbfddc7dc143c76703c7fde0ff5c207da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 27 May 2015 13:11:32 +0200 Subject: [PATCH 218/398] Big refactoring/rewrite for how annotation tooltips are shown, also work on #1474 --- ' | 36 +++++++++++++++++ public/app/components/extend-jquery.js | 16 +++++++- public/app/directives/all.js | 1 + public/app/directives/annotationTooltip.js | 40 +++++++++++++++++++ public/app/directives/tags.js | 1 - .../features/annotations/annotationsSrv.js | 24 ++--------- public/app/features/dashboard/timeSrv.js | 2 +- public/app/partials/search.html | 2 +- .../plugins/datasource/graphite/datasource.js | 10 ++++- public/css/less/graph.less | 5 +++ public/vendor/jquery/jquery.flot.events.js | 8 ++-- 11 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 ' create mode 100644 public/app/directives/annotationTooltip.js diff --git a/' b/' new file mode 100644 index 00000000000..b179a2b8b5a --- /dev/null +++ b/' @@ -0,0 +1,36 @@ +define([ + 'angular', + 'lodash' +], +function (angular) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('annotationTooltip', function($sanitize, dashboardSrv) { + return { + scope: { tagColorFromName: "=" }, + link: function (scope, element) { + var title = $sanitize(scope.annoation.title); + var dashboard = dashboardSrv.getCurrent(); + var time = '' + dashboard.formatDate(scope.annotation.time) + ''; + + var tooltip = '
    '+ title + ' ' + time + '
    ' ; + + if (options.tags) { + var tags = $sanitize(options.tags); + tooltip += '' + (tags || '') + '
    '; + } + + if (options.text) { + var text = $sanitize(options.text); + tooltip += text.replace(/\n/g, '
    '); + } + + tooltip += "
    "; + } + }; + }); + +}); + diff --git a/public/app/components/extend-jquery.js b/public/app/components/extend-jquery.js index ce5c8afb1a9..3e1f6b0c054 100644 --- a/public/app/components/extend-jquery.js +++ b/public/app/components/extend-jquery.js @@ -1,5 +1,5 @@ -define(['jquery'], -function ($) { +define(['jquery', 'angular', 'lodash'], +function ($, angular, _) { 'use strict'; /** @@ -14,6 +14,7 @@ function ($) { return function (x, y, opts) { opts = $.extend(true, {}, defaults, opts); + return this.each(function () { var $tooltip = $(this), width, height; @@ -22,6 +23,17 @@ function ($) { $("#tooltip").remove(); $tooltip.appendTo(document.body); + if (opts.compile) { + angular.element(document).injector().invoke(function($compile, $rootScope) { + var tmpScope = $rootScope.$new(true); + _.extend(tmpScope, opts.scopeData); + + $compile($tooltip)(tmpScope); + tmpScope.$digest(); + //tmpScope.$destroy(); + }); + } + width = $tooltip.outerWidth(true); height = $tooltip.outerHeight(true); diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 7e95b6f26dc..b92bc59ca41 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -17,4 +17,5 @@ define([ './dropdown.typeahead', './topnav', './giveFocus', + './annotationTooltip', ], function () {}); diff --git a/public/app/directives/annotationTooltip.js b/public/app/directives/annotationTooltip.js new file mode 100644 index 00000000000..f84df88dd26 --- /dev/null +++ b/public/app/directives/annotationTooltip.js @@ -0,0 +1,40 @@ +define([ + 'angular', + 'jquery', + 'lodash' +], +function (angular, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('annotationTooltip', function($sanitize, dashboardSrv, $compile) { + return { + link: function (scope, element) { + var event = scope.event; + var title = $sanitize(event.title); + var dashboard = dashboardSrv.getCurrent(); + var time = '' + dashboard.formatDate(event.min) + ''; + + var tooltip = '
    ' + title + ' ' + time + '
    ' ; + + if (event.text) { + var text = $sanitize(event.text); + tooltip += text.replace(/\n/g, '
    ') + '
    '; + } + + if (event.tags && event.tags.length > 0) { + tooltip += '{{tag}}
    '; + } + + tooltip += "
    "; + + var $tooltip = $(tooltip); + $tooltip.appendTo(element); + + $compile(element.contents())(scope); + } + }; + }); + +}); diff --git a/public/app/directives/tags.js b/public/app/directives/tags.js index 3f77fc6ba12..4f8825a010b 100644 --- a/public/app/directives/tags.js +++ b/public/app/directives/tags.js @@ -41,7 +41,6 @@ function (angular, $) { angular .module('grafana.directives') .directive('tagColorFromName', function() { - return { scope: { tagColorFromName: "=" }, link: function (scope, element) { diff --git a/public/app/features/annotations/annotationsSrv.js b/public/app/features/annotations/annotationsSrv.js index 0ba30f1ef8b..a4529de2019 100644 --- a/public/app/features/annotations/annotationsSrv.js +++ b/public/app/features/annotations/annotationsSrv.js @@ -7,7 +7,7 @@ define([ var module = angular.module('grafana.services'); - module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope, $sanitize) { + module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) { var promiseCached; var list = []; var self = this; @@ -57,30 +57,14 @@ define([ }; this.addAnnotation = function(options) { - var title = $sanitize(options.title); - var time = '' + self.dashboard.formatDate(options.time) + ''; - - var tooltip = '
    '+ title + ' ' + time + '
    ' ; - - if (options.tags) { - var tags = $sanitize(options.tags); - tooltip += '' + (tags || '') + '
    '; - } - - if (options.text) { - var text = $sanitize(options.text); - tooltip += text.replace(/\n/g, '
    '); - } - - tooltip += ""; - list.push({ annotation: options.annotation, min: options.time, max: options.time, eventType: options.annotation.name, - title: null, - description: tooltip, + title: options.title, + tags: options.tags, + text: options.text, score: 1 }); }; diff --git a/public/app/features/dashboard/timeSrv.js b/public/app/features/dashboard/timeSrv.js index 7df83f7fcab..6bb9ccde223 100644 --- a/public/app/features/dashboard/timeSrv.js +++ b/public/app/features/dashboard/timeSrv.js @@ -93,7 +93,7 @@ define([ _.extend(this.time, time); // disable refresh if we have an absolute time - if (time.to && time.to.indexOf('now') === -1) { + if (_.isString(time.to) && time.to.indexOf('now') === -1) { this.old_refresh = this.dashboard.refresh || this.old_refresh; this.set_interval(false); } diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 18754010f0d..9c644e96d25 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -16,7 +16,7 @@ tags - | + | {{query.tag}} diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index dfd096cdb0a..3697cade07b 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -111,11 +111,19 @@ function (angular, _, $, config, kbn, moment) { var list = []; for (var i = 0; i < results.data.length; i++) { var e = results.data[i]; + var tags = []; + if (e.tags) { + tags = e.tags.split(','); + if (tags.length === 1) { + tags = e.tags.split(' '); + } + } + list.push({ annotation: annotation, time: e.when * 1000, title: e.what, - tags: e.tags, + tags: tags, text: e.data }); } diff --git a/public/css/less/graph.less b/public/css/less/graph.less index b96e466eea1..a0350d5d16c 100644 --- a/public/css/less/graph.less +++ b/public/css/less/graph.less @@ -212,6 +212,11 @@ top: -3px; } + .label-tag { + margin-right: 4px; + margin-top: 8px; + } + .graph-tooltip-list-item { display: table-row; } diff --git a/public/vendor/jquery/jquery.flot.events.js b/public/vendor/jquery/jquery.flot.events.js index 4924b16d03b..48ebf2b67fd 100644 --- a/public/vendor/jquery/jquery.flot.events.js +++ b/public/vendor/jquery/jquery.flot.events.js @@ -191,14 +191,12 @@ console.log(tooltip); */ - // @rashidkpc - hack to work with our normal tooltip placer - var $tooltip = $('
    '); + // grafana addition + var $tooltip = $('
    '); if (event) { $tooltip .html(event.description) - .place_tt(x, y, { - offset: 10 - }); + .place_tt(x, y, {offset: 10, compile: true, scopeData: {event: event}}); } else { $tooltip.remove(); } From 96bd66e8119e79e3b4da1104abeb662d618a8ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 27 May 2015 14:30:23 +0200 Subject: [PATCH 219/398] Made the annotation tags support more cross datasource compatible --- public/app/directives/annotationTooltip.js | 15 ++++++++++++--- .../app/plugins/datasource/graphite/datasource.js | 9 +-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/directives/annotationTooltip.js b/public/app/directives/annotationTooltip.js index f84df88dd26..25059d08274 100644 --- a/public/app/directives/annotationTooltip.js +++ b/public/app/directives/annotationTooltip.js @@ -3,7 +3,7 @@ define([ 'jquery', 'lodash' ], -function (angular, $) { +function (angular, $, _) { 'use strict'; angular @@ -23,8 +23,17 @@ function (angular, $) { tooltip += text.replace(/\n/g, '
    ') + '
    '; } - if (event.tags && event.tags.length > 0) { - tooltip += '{{tag}}
    '; + var tags = event.tags; + if (_.isString(event.tags)) { + tags = event.tags.split(','); + if (tags.length === 1) { + tags = event.tags.split(' '); + } + } + + if (tags && tags.length) { + scope.tags = tags; + tooltip += '{{tag}}
    '; } tooltip += "
    "; diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index 3697cade07b..9315b5a5b33 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -111,19 +111,12 @@ function (angular, _, $, config, kbn, moment) { var list = []; for (var i = 0; i < results.data.length; i++) { var e = results.data[i]; - var tags = []; - if (e.tags) { - tags = e.tags.split(','); - if (tags.length === 1) { - tags = e.tags.split(' '); - } - } list.push({ annotation: annotation, time: e.when * 1000, title: e.what, - tags: tags, + tags: e.tags, text: e.data }); } From aeb8bc875584ae884adafcea3b3ab2074ded0a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 09:35:47 +0200 Subject: [PATCH 220/398] Share link should always have absolute time range, Closes #2060 --- .../app/features/dashboard/shareModalCtrl.js | 6 ++--- public/test/specs/shareModalCtrl-specs.js | 26 +++++-------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 1dd99dbb126..55ec0c8a410 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -43,9 +43,9 @@ function (angular, _, require, config) { var params = angular.copy($location.search()); - var range = timeSrv.timeRangeForUrl(); - params.from = range.from; - params.to = range.to; + var range = timeSrv.timeRange(); + params.from = range.from.getTime(); + params.to = range.to.getTime(); if ($scope.options.includeTemplateVars) { templateSrv.fillVariableValuesForUrl(params); diff --git a/public/test/specs/shareModalCtrl-specs.js b/public/test/specs/shareModalCtrl-specs.js index d0d69479a13..c9d5131d11a 100644 --- a/public/test/specs/shareModalCtrl-specs.js +++ b/public/test/specs/shareModalCtrl-specs.js @@ -9,10 +9,10 @@ define([ var ctx = new helpers.ControllerTestContext(); function setTime(range) { - ctx.timeSrv.timeRangeForUrl = sinon.stub().returns(range); + ctx.timeSrv.timeRange = sinon.stub().returns(range); } - setTime({ from: 'now-1h', to: 'now' }); + setTime({ from: new Date(1000), to: new Date(2000) }); beforeEach(module('grafana.controllers')); beforeEach(module('grafana.services')); @@ -23,57 +23,43 @@ define([ describe('shareUrl with current time range and panel', function() { - it('should generate share url relative time', function() { - ctx.$location.path('/test'); - ctx.scope.panel = { id: 22 }; - - setTime({ from: 'now-1h', to: 'now' }); - - ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&panelId=22&fullscreen'); - }); - it('should generate share url absolute time', function() { ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; - setTime({ from: 1362178800000, to: 1396648800000 }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1362178800000&to=1396648800000&panelId=22&fullscreen'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&panelId=22&fullscreen'); }); it('should remove panel id when no panel in scope', function() { ctx.$location.path('/test'); ctx.scope.options.forCurrent = true; ctx.scope.panel = null; - setTime({ from: 'now-1h', to: 'now' }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000'); }); it('should add theme when specified', function() { ctx.$location.path('/test'); ctx.scope.options.theme = 'light'; ctx.scope.panel = null; - setTime({ from: 'now-1h', to: 'now' }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&theme=light'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&theme=light'); }); it('should include template variables in url', function() { ctx.$location.path('/test'); ctx.scope.options.includeTemplateVars = true; - setTime({ from: 'now-1h', to: 'now' }); ctx.templateSrv.fillVariableValuesForUrl = function(params) { params['var-app'] = 'mupp'; params['var-server'] = 'srv-01'; }; ctx.scope.buildUrl(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&var-app=mupp&var-server=srv-01'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&var-app=mupp&var-server=srv-01'); }); }); From 57fac6b9aaf0a10b36a5f2a71582fb113da738aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:41:32 +0200 Subject: [PATCH 221/398] Removed invalid scripted dashboard example, Closes #2063 --- public/dashboards/scripted_gen_and_save.js | 95 ---------------------- 1 file changed, 95 deletions(-) delete mode 100644 public/dashboards/scripted_gen_and_save.js diff --git a/public/dashboards/scripted_gen_and_save.js b/public/dashboards/scripted_gen_and_save.js deleted file mode 100644 index b7ad24bc25c..00000000000 --- a/public/dashboards/scripted_gen_and_save.js +++ /dev/null @@ -1,95 +0,0 @@ -/* global _ */ - -/* - * Complex scripted dashboard - * This script generates a dashboard object that Grafana can load. It also takes a number of user - * supplied URL parameters (in the ARGS variable) - * - * Return a dashboard object, or a function - * - * For async scripts, return a function, this function must take a single callback function as argument, - * call this callback function with the dashboard object (look at scripted_async.js for an example) - */ - -'use strict'; - -// accessible variables in this scope -var window, document, ARGS, $, jQuery, moment, kbn, services, _; - -// default datasource -var datasource = services.datasourceSrv.default; -// get datasource used for saving dashboards -var dashboardDB = services.datasourceSrv.getGrafanaDB(); - -var targets = []; - -function getTargets(path) { - return datasource.metricFindQuery(path + '.*').then(function(result) { - if (!result) { - return null; - } - - if (targets.length === 10) { - return null; - } - - var promises = _.map(result, function(metric) { - if (metric.expandable) { - return getTargets(path + "." + metric.text); - } - else { - targets.push(path + '.' + metric.text); - } - return null; - }); - - return services.$q.all(promises); - }); -} - -function createDashboard(target, index) { - // Intialize a skeleton with nothing but a rows array and service object - var dashboard = { rows : [] }; - dashboard.title = 'Scripted dash ' + index; - dashboard.time = { - from: "now-6h", - to: "now" - }; - - dashboard.rows.push({ - title: 'Chart', - height: '300px', - panels: [ - { - title: 'Events', - type: 'graph', - span: 12, - targets: [ {target: target} ] - } - ] - }); - - return dashboard; -} - -function saveDashboard(dashboard) { - var model = services.dashboardSrv.create(dashboard); - dashboardDB.saveDashboard(model); -} - -return function(callback) { - - getTargets('apps').then(function() { - console.log('targets: ', targets); - _.each(targets, function(target, index) { - var dashboard = createDashboard(target, index); - saveDashboard(dashboard); - - if (index === targets.length - 1) { - callback(dashboard); - } - }); - }); - -}; - From cf147cdeafb4e299984e0552d423bad4d7660481 Mon Sep 17 00:00:00 2001 From: Andrea Bernardo Ciddio Date: Thu, 28 May 2015 16:47:20 +0100 Subject: [PATCH 222/398] GitHub users without a public email should be authenticated using their primary private email address --- pkg/social/social.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/social/social.go b/pkg/social/social.go index 49812ddd87f..1a00934b937 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -186,6 +186,37 @@ func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { return false } +func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) { + type Record struct { + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` + } + + emailsUrl := fmt.Sprintf("https://api.github.com/user/emails") + r, err := client.Get(emailsUrl) + if err != nil { + return "", err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return "", err + } + + var email = "" + for _, record := range records { + if record.Primary { + email = record.Email + } + } + + return email, nil +} + func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { type Record struct { Id int `json:"id"` @@ -274,6 +305,13 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, ErrMissingOrganizationMembership } + if userInfo.Email == "" { + userInfo.Email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } + } + return userInfo, nil } From 0108dfa80327396545c8a00b1ddfb402602b3acc Mon Sep 17 00:00:00 2001 From: yinchuan Date: Fri, 29 May 2015 11:23:14 +0800 Subject: [PATCH 223/398] Update configuration.md --- docs/sources/installation/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4665803960a..6e13398566b 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -296,12 +296,12 @@ Secret. Specify these in the Grafana configuration file. For example: scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email auth_url = https://accounts.google.com/o/oauth2/auth token_url = https://accounts.google.com/o/oauth2/token - allowed_domains = mycompany.com + allowed_domains = mycompany.com mycompany.org allow_sign_up = false Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional. +accounts. The `allowed_domains` option is optional, and domains is seperated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any From ed974a808bac3d5cf5f77b387185eb3b2770e273 Mon Sep 17 00:00:00 2001 From: yinchuan Date: Fri, 29 May 2015 11:39:59 +0800 Subject: [PATCH 224/398] Update configuration.md --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 6e13398566b..e27a3e80f6a 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -301,7 +301,7 @@ Secret. Specify these in the Grafana configuration file. For example: Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains is seperated by space. +accounts. The `allowed_domains` option is optional, and domains were seperated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any From fc43ce657c91a7bcaaeb95c1e806b60dc3c6cc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 11:00:05 +0200 Subject: [PATCH 225/398] allow data source proxy to proxy requests over self signed https connections, Closes #2069 --- pkg/api/dataproxy.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 81318cdc536..11075294b66 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,9 +1,12 @@ package api import ( + "crypto/tls" + "net" "net/http" "net/http/httputil" "net/url" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" @@ -11,6 +14,16 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var dataProxyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy { target, _ := url.Parse(ds.Url) @@ -56,5 +69,6 @@ func ProxyDataSourceRequest(c *middleware.Context) { proxyPath := c.Params("*") proxy := NewReverseProxy(&query.Result, proxyPath) + proxy.Transport = dataProxyTransport proxy.ServeHTTP(c.RW(), c.Req.Request) } From e2f6633d57624664654463578e8d2502bfd7ffef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 12:15:49 +0200 Subject: [PATCH 226/398] Began work on data source test / validation, #1997 & #2043 --- CHANGELOG.md | 1 + pkg/api/api.go | 7 ++- pkg/api/datasources.go | 4 +- pkg/models/datasource.go | 2 +- public/app/features/org/datasourceEditCtrl.js | 46 +++++++++++++------ .../features/org/partials/datasourceEdit.html | 21 +++++++-- .../plugins/datasource/graphite/datasource.js | 16 +++++++ public/css/less/overrides.less | 45 +++++++++--------- 8 files changed, 95 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6265738ef..f03c7f34891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing - Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) - Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI +- Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/api/api.go b/pkg/api/api.go index f5da406a8a5..6ecaa51652e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -107,10 +107,9 @@ func Register(r *macaron.Macaron) { // Data sources r.Group("/datasources", func() { - r.Combo("/"). - Get(GetDataSources). - Put(bind(m.AddDataSourceCommand{}), AddDataSource). - Post(bind(m.UpdateDataSourceCommand{}), UpdateDataSource) + r.Get("/", GetDataSources) + r.Post("/", bind(m.AddDataSourceCommand{}), AddDataSource) + r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource) r.Delete("/:id", DeleteDataSource) r.Get("/:id", GetDataSourceById) r.Get("/plugins", GetDataSourcePlugins) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 76a9bddd253..e0253df3cdb 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/util" ) func GetDataSources(c *middleware.Context) { @@ -94,11 +95,12 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { return } - c.JsonOK("Datasource added") + c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id}) } func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { cmd.OrgId = c.OrgId + cmd.Id = c.ParamsInt64(":id") err := bus.Dispatch(&cmd) if err != nil { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 2ba236cd56b..c756faaba59 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -69,7 +69,6 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { - Id int64 `json:"id" binding:"Required"` Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` Access DsAccess `json:"access" binding:"Required"` @@ -84,6 +83,7 @@ type UpdateDataSourceCommand struct { JsonData map[string]interface{} `json:"jsonData"` OrgId int64 `json:"-"` + Id int64 `json:"-"` } type DeleteDataSourceCommand struct { diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index 780aee3e843..12d552c985b 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -25,7 +25,6 @@ function (angular, config) { $scope.loadDatasourceTypes().then(function() { if ($routeParams.id) { - $scope.isNew = false; $scope.getDatasourceById($routeParams.id); } else { $scope.current = angular.copy(defaults); @@ -48,6 +47,7 @@ function (angular, config) { $scope.getDatasourceById = function(id) { backendSrv.get('/api/datasources/' + id).then(function(ds) { + $scope.isNew = false; $scope.current = ds; $scope.typeChanged(); }); @@ -65,26 +65,46 @@ function (angular, config) { }); }; - $scope.update = function() { - if (!$scope.editForm.$valid) { - return; - } + $scope.testDatasource = function() { + $scope.testing = { done: false }; - backendSrv.post('/api/datasources', $scope.current).then(function() { - $scope.updateFrontendSettings(); - $location.path("datasources"); + datasourceSrv.get($scope.current.name).then(function(datasource) { + if (!datasource.testDatasource) { + $scope.testing.message = 'Data source does not support test connection feature.'; + $scope.testing.status = 'warning'; + $scope.testing.title = 'Unknown'; + return; + } + return datasource.testDatasource().then(function(result) { + $scope.testing.message = result.message; + $scope.testing.status = result.status; + $scope.testing.title = result.title; + }); + }).finally(function() { + $scope.testing.done = true; }); }; - $scope.add = function() { + $scope.saveChanges = function(test) { if (!$scope.editForm.$valid) { return; } - backendSrv.put('/api/datasources', $scope.current).then(function() { - $scope.updateFrontendSettings(); - $location.path("datasources"); - }); + if ($scope.current.id) { + return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() { + $scope.updateFrontendSettings(); + if (test) { + $scope.testDatasource(); + } else { + $location.path('datasources'); + } + }); + } else { + return backendSrv.post('/api/datasources', $scope.current).then(function(result) { + $scope.updateFrontendSettings(); + $location.path('datasources/edit/' + result.id); + }); + } }; $scope.init(); diff --git a/public/app/features/org/partials/datasourceEdit.html b/public/app/features/org/partials/datasourceEdit.html index 12b46ee284b..6ea33e5a43c 100644 --- a/public/app/features/org/partials/datasourceEdit.html +++ b/public/app/features/org/partials/datasourceEdit.html @@ -43,11 +43,22 @@
    -
    -
    -
    - - + +
    +
    Testing....
    +
    Test results
    +
    +
    {{testing.title}}
    +
    +
    +
    + +
    + + + Cancel

    diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index 9315b5a5b33..ff06d9f46aa 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -196,6 +196,22 @@ function (angular, _, $, config, kbn, moment) { }); }; + GraphiteDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('*').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }, function(err) { + var message, title; + if (err.statusText) { + message = err.statusText; + title = "HTTP Error"; + } else { + message = err; + title = "Unknown error"; + } + return { status: "error", message: message, title: title }; + }); + }; + GraphiteDatasource.prototype.listDashboards = function(query) { return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} }) .then(function(results) { diff --git a/public/css/less/overrides.less b/public/css/less/overrides.less index 425777f1597..b03cb7f7e6b 100644 --- a/public/css/less/overrides.less +++ b/public/css/less/overrides.less @@ -315,38 +315,37 @@ div.flot-text { position: fixed; right: 20px; top: 56px; +} - .alert { - color: @white; - padding-bottom: 13px; - position: relative; - } +.alert { + color: @white; + padding-bottom: 13px; + position: relative; +} - .alert-close { - position: absolute; - top: -4px; - right: -2px; - width: 19px; - height: 19px; - padding: 0; - background: @grayLighter; - border-radius: 50%; - border: none; - font-size: 1.1rem; - color: @grayDarker; - } +.alert-close { + position: absolute; + top: -4px; + right: -2px; + width: 19px; + height: 19px; + padding: 0; + background: @grayLighter; + border-radius: 50%; + border: none; + font-size: 1.1rem; + color: @grayDarker; +} - .alert-title { - font-weight: bold; - padding-bottom: 2px; - } +.alert-title { + font-weight: bold; + padding-bottom: 2px; } .alert-warning { background-color: @warningBackground; border-color: @warningBorder; - color: @warningText; } /* =================================================== From afede880e6900ad192fb8ba0c9fece2e7c5f3fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 14:02:24 +0200 Subject: [PATCH 227/398] added url validation when adding data source, Fixes #2043 --- .../org/partials/datasourceHttpConfig.html | 2 +- .../plugins/datasource/influxdb/datasource.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/app/features/org/partials/datasourceHttpConfig.html b/public/app/features/org/partials/datasourceHttpConfig.html index 88923e77b3d..94c208d8370 100644 --- a/public/app/features/org/partials/datasourceHttpConfig.html +++ b/public/app/features/org/partials/datasourceHttpConfig.html @@ -6,7 +6,7 @@ Url
  • - +
  • Access Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 2ff0f8beb0e..c483e28b288 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -126,6 +126,22 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return this._influxRequest('GET', '/query', {q: query}); }; + InfluxDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }, function(err) { + var message, title; + if (err.statusText) { + message = err.statusText; + title = "HTTP Error"; + } else { + message = err; + title = "Unknown error"; + } + return { status: "error", message: message, title: title }; + }); + }; + InfluxDatasource.prototype._influxRequest = function(method, url, data) { var self = this; var deferred = $q.defer(); From 50645cc36bfdec608c12f36a2acfc610e5b08df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 14:16:59 +0200 Subject: [PATCH 228/398] Added test connection action to all data sources, Closes #1997 --- public/app/features/org/datasourceEditCtrl.js | 9 +++++++++ public/app/plugins/datasource/graphite/datasource.js | 10 ---------- public/app/plugins/datasource/influxdb/datasource.js | 10 ---------- .../app/plugins/datasource/influxdb_08/datasource.js | 6 ++++++ public/app/plugins/datasource/opentsdb/datasource.js | 6 ++++++ 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index 12d552c985b..f2af6325a20 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -75,10 +75,19 @@ function (angular, config) { $scope.testing.title = 'Unknown'; return; } + return datasource.testDatasource().then(function(result) { $scope.testing.message = result.message; $scope.testing.status = result.status; $scope.testing.title = result.title; + }, function(err) { + if (err.statusText) { + $scope.testing.message = err.statusText; + $scope.testing.title = "HTTP Error"; + } else { + $scope.testing.message = err.message; + $scope.testing.title = "Unknown error"; + } }); }).finally(function() { $scope.testing.done = true; diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index ff06d9f46aa..a3255584879 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -199,16 +199,6 @@ function (angular, _, $, config, kbn, moment) { GraphiteDatasource.prototype.testDatasource = function() { return this.metricFindQuery('*').then(function () { return { status: "success", message: "Data source is working", title: "Success" }; - }, function(err) { - var message, title; - if (err.statusText) { - message = err.statusText; - title = "HTTP Error"; - } else { - message = err; - title = "Unknown error"; - } - return { status: "error", message: message, title: title }; }); }; diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index c483e28b288..a8656114fdf 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -129,16 +129,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { InfluxDatasource.prototype.testDatasource = function() { return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { return { status: "success", message: "Data source is working", title: "Success" }; - }, function(err) { - var message, title; - if (err.statusText) { - message = err.statusText; - title = "HTTP Error"; - } else { - message = err; - title = "Unknown error"; - } - return { status: "error", message: message, title: title }; }); }; diff --git a/public/app/plugins/datasource/influxdb_08/datasource.js b/public/app/plugins/datasource/influxdb_08/datasource.js index 8ff01c8553f..0e4adba072c 100644 --- a/public/app/plugins/datasource/influxdb_08/datasource.js +++ b/public/app/plugins/datasource/influxdb_08/datasource.js @@ -99,6 +99,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { }); }; + InfluxDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('list series').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }); + }; + InfluxDatasource.prototype.metricFindQuery = function (query) { var interpolated; try { diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 936500be0f0..1e60bf1e54e 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -90,6 +90,12 @@ function (angular, _, kbn) { }); }; + OpenTSDBDatasource.prototype.testDatasource = function() { + return this.performSuggestQuery('cpu', 'metrics').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }); + }; + function transformMetricData(md, groupByTags, options) { var metricLabel = createMetricLabel(md, options, groupByTags); var dps = []; From 85c3a0aa1441bd37c7fc455ab51effbc7bf53741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:29:45 +0200 Subject: [PATCH 229/398] Panel menu now hides edit actions for users with role Viewer, Closes #1826 --- ' | 36 ------------- pkg/api/dashboard.go | 1 + pkg/api/dtos/models.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 2 + public/app/components/panelmeta.js | 12 ++--- public/app/features/panel/panelMenu.js | 70 +++++++++++++++----------- public/app/services/contextSrv.js | 2 +- public/css/less/panel.less | 1 - 9 files changed, 54 insertions(+), 72 deletions(-) delete mode 100644 ' diff --git a/' b/' deleted file mode 100644 index b179a2b8b5a..00000000000 --- a/' +++ /dev/null @@ -1,36 +0,0 @@ -define([ - 'angular', - 'lodash' -], -function (angular) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('annotationTooltip', function($sanitize, dashboardSrv) { - return { - scope: { tagColorFromName: "=" }, - link: function (scope, element) { - var title = $sanitize(scope.annoation.title); - var dashboard = dashboardSrv.getCurrent(); - var time = '' + dashboard.formatDate(scope.annotation.time) + ''; - - var tooltip = '
    '+ title + ' ' + time + '
    ' ; - - if (options.tags) { - var tags = $sanitize(options.tags); - tooltip += '' + (tags || '') + '
    '; - } - - if (options.text) { - var text = $sanitize(options.text); - tooltip += text.replace(/\n/g, '
    '); - } - - tooltip += ""; - } - }; - }); - -}); - diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index b439bd67ac7..00ae26744d9 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -55,6 +55,7 @@ func GetDashboard(c *middleware.Context) { Type: m.DashTypeDB, CanStar: c.IsSignedIn, CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, + CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, }, } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d5e294cc983..3e1826f56fb 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -34,6 +34,7 @@ type DashboardMeta struct { IsSnapshot bool `json:"isSnapshot,omitempty"` Type string `json:"type,omitempty"` CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` CanStar bool `json:"canStar"` Slug string `json:"slug"` Expires time.Time `json:"expires"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ea154320608..4dd6ba06819 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -99,6 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "defaultDatasource": defaultDatasource, "datasources": datasources, "appSubUrl": setting.AppSubUrl, + "viewerRoleMode": setting.ViewerRoleMode, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, "commit": setting.BuildCommit, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 77445dd6c4c..6768f9aabd9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -79,6 +79,7 @@ var ( AllowUserOrgCreate bool AutoAssignOrg bool AutoAssignOrgRole string + ViewerRoleMode string // Http auth AdminUser string @@ -383,6 +384,7 @@ func NewConfigContext(args *CommandLineArgs) { AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) + ViewerRoleMode = users.Key("viewer_role_mode").In("default", []string{"default", "strinct"}) // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) diff --git a/public/app/components/panelmeta.js b/public/app/components/panelmeta.js index 4ee4a9b9b55..013919c174a 100644 --- a/public/app/components/panelmeta.js +++ b/public/app/components/panelmeta.js @@ -16,8 +16,8 @@ function () { this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false); dismiss();'); } - this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();'); - this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()'); + this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor'); + this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()', 'Editor'); this.addMenuItem('share', 'icon-share', 'sharePanel(); dismiss();'); this.addEditorTab('General', 'app/partials/panelgeneral.html'); @@ -29,12 +29,12 @@ function () { this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();'); } - PanelMeta.prototype.addMenuItem = function(text, icon, click) { - this.menu.push({text: text, icon: icon, click: click}); + PanelMeta.prototype.addMenuItem = function(text, icon, click, role) { + this.menu.push({text: text, icon: icon, click: click, role: role}); }; - PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) { - this.extendedMenu.push({text: text, icon: icon, click: click}); + PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click, role) { + this.extendedMenu.push({text: text, icon: icon, click: click, role: role}); }; PanelMeta.prototype.addEditorTab = function(title, src) { diff --git a/public/app/features/panel/panelMenu.js b/public/app/features/panel/panelMenu.js index 27152ef2b94..03ee78491ab 100644 --- a/public/app/features/panel/panelMenu.js +++ b/public/app/features/panel/panelMenu.js @@ -8,7 +8,7 @@ function (angular, $, _) { angular .module('grafana.directives') - .directive('panelMenu', function($compile, linkSrv) { + .directive('panelMenu', function($compile, linkSrv, contextSrv) { var linkTemplate = '' + '{{panel.title | interpolateTemplateVars:this}}' + @@ -18,18 +18,26 @@ function (angular, $, _) { function createMenuTemplate($scope) { var template = '
    '; - template += '
    '; - template += '
    '; - template += ''; - template += ''; - template += ''; - template += '
    '; - template += '
    '; + + if ($scope.dashboardMeta.canEdit && contextSrv.isEditor) { + template += '
    '; + template += '
    '; + template += ''; + template += ''; + template += ''; + template += '
    '; + template += '
    '; + } template += '
    '; template += ''; _.each($scope.panelMeta.menu, function(item) { + // skip edit actions if not editor + if (item.role === 'Editor' && (!contextSrv.isEditor || !$scope.dashboardMeta.canEdit)) { + return; + } + template += ' 0) { - menuLeftPos -= stickingOut + 10; - } - if (panelLeftPos + menuLeftPos < 0) { - menuLeftPos = 0; - } - var menuTemplate = createMenuTemplate($scope); $menu = $(menuTemplate); - $menu.css('left', menuLeftPos); $menu.mouseleave(function() { dismiss(1000); }); @@ -136,15 +130,35 @@ function (angular, $, _) { dismiss(null, true); }; - $('.panel-menu').remove(); - elem.append($menu); - $scope.$apply(function() { - $compile($menu.contents())(menuScope); - }); - $(".panel-container").removeClass('panel-highlight'); $panelContainer.toggleClass('panel-highlight'); + $('.panel-menu').remove(); + + elem.append($menu); + + $scope.$apply(function() { + $compile($menu.contents())(menuScope); + + var menuWidth = $menu[0].offsetWidth; + var menuHeight = $menu[0].offsetHeight; + + var windowWidth = $(window).width(); + var panelLeftPos = $(elem).offset().left; + var panelWidth = $(elem).width(); + + var menuLeftPos = (panelWidth / 2) - (menuWidth/2); + var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth; + if (stickingOut > 0) { + menuLeftPos -= stickingOut + 10; + } + if (panelLeftPos + menuLeftPos < 0) { + menuLeftPos = 0; + } + + $menu.css({'left': menuLeftPos, top: -menuHeight}); + }); + dismiss(2200); }; diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index b3f8a1ed164..aa844ee5113 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -60,6 +60,6 @@ function (angular, _, store, config) { store.set('grafana.sidemenu', false); } - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin') || this.hasRole('Read Only Editor'); }); }); diff --git a/public/css/less/panel.less b/public/css/less/panel.less index 17b8ff0cd52..6f011b2978f 100644 --- a/public/css/less/panel.less +++ b/public/css/less/panel.less @@ -130,7 +130,6 @@ position: absolute; background: @grafanaTargetFuncBackground; border: 1px solid black; - top: -62px; .panel-menu-row { white-space: nowrap; From 83e7c48767ce8f07d4e53e05af9de4ae705cfa9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:39:01 +0200 Subject: [PATCH 230/398] User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role will replace the old Viewer behavior --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03c7f34891..bc4c86878d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - [Issue #960](https://github.com/grafana/grafana/issues/960). Search: Backend can now index a folder with json files, will be available in search (saving back to folder is not supported, this feature is meant for static generated json dashboards) **Breaking changes** +- [Issue #1826](https://github.com/grafana/grafana/issues/1826). User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role `Read Only Editor` will replace the old Viewer behavior - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing - Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) - Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI From 86f5152092cb28051566107d76df2d3d11883fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:49:14 +0200 Subject: [PATCH 231/398] When role is viewer and edit URL is loaded view mode will be loaded instead, Closes #2089 --- public/app/features/dashboard/viewStateSrv.js | 7 ++++--- public/app/features/panel/panelMenu.js | 6 +++--- public/app/features/panel/panelSrv.js | 8 -------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index ef7669e387c..e87a942a6de 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -130,10 +130,11 @@ function (angular, _, $) { var docHeight = $(window).height(); var editHeight = Math.floor(docHeight * 0.3); var fullscreenHeight = Math.floor(docHeight * 0.7); - this.oldTimeRange = panelScope.range; - panelScope.height = this.state.edit ? editHeight : fullscreenHeight; - panelScope.editMode = this.state.edit; + panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit; + panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight; + + this.oldTimeRange = panelScope.range; this.fullscreenPanel = panelScope; $(window).scrollTop(0); diff --git a/public/app/features/panel/panelMenu.js b/public/app/features/panel/panelMenu.js index 03ee78491ab..5a701084ff3 100644 --- a/public/app/features/panel/panelMenu.js +++ b/public/app/features/panel/panelMenu.js @@ -8,7 +8,7 @@ function (angular, $, _) { angular .module('grafana.directives') - .directive('panelMenu', function($compile, linkSrv, contextSrv) { + .directive('panelMenu', function($compile, linkSrv) { var linkTemplate = '' + '{{panel.title | interpolateTemplateVars:this}}' + @@ -19,7 +19,7 @@ function (angular, $, _) { function createMenuTemplate($scope) { var template = '
    '; - if ($scope.dashboardMeta.canEdit && contextSrv.isEditor) { + if ($scope.dashboardMeta.canEdit) { template += '
    '; template += '
    '; template += ''; @@ -34,7 +34,7 @@ function (angular, $, _) { _.each($scope.panelMeta.menu, function(item) { // skip edit actions if not editor - if (item.role === 'Editor' && (!contextSrv.isEditor || !$scope.dashboardMeta.canEdit)) { + if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) { return; } diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index d29866033ee..b863518ff72 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -71,14 +71,6 @@ function (angular, _, config) { }; $scope.toggleFullscreen = function(edit) { - if (edit && $scope.dashboardMeta.canEdit === false) { - $scope.appEvent('alert-warning', [ - 'Dashboard not editable', - 'Use Save As.. feature to create an editable copy of this dashboard.' - ]); - return; - } - $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id }); }; From ff3843bc7fe894178c21953447dae6a16985bf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 17:01:04 +0200 Subject: [PATCH 232/398] Roles: New user role that replaces the old role behavior, Closes #2088 --- CHANGELOG.md | 1 + pkg/api/dashboard.go | 2 +- pkg/models/org_user.go | 9 +++++---- public/app/features/org/partials/orgUsers.html | 6 +++--- public/app/services/contextSrv.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4c86878d4..9096744be61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). +- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 00ae26744d9..b010f32cdcb 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -55,7 +55,7 @@ func GetDashboard(c *middleware.Context) { Type: m.DashTypeDB, CanStar: c.IsSignedIn, CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, - CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, + CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_READ_ONLY_EDITOR, }, } diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 3e40fd24b68..afbb10386c8 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -15,13 +15,14 @@ var ( type RoleType string const ( - ROLE_VIEWER RoleType = "Viewer" - ROLE_EDITOR RoleType = "Editor" - ROLE_ADMIN RoleType = "Admin" + ROLE_VIEWER RoleType = "Viewer" + ROLE_EDITOR RoleType = "Editor" + ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor" + ROLE_ADMIN RoleType = "Admin" ) func (r RoleType) IsValid() bool { - return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR + return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR } type OrgUser struct { diff --git a/public/app/features/org/partials/orgUsers.html b/public/app/features/org/partials/orgUsers.html index b32ff031caa..b32ffb67081 100644 --- a/public/app/features/org/partials/orgUsers.html +++ b/public/app/features/org/partials/orgUsers.html @@ -12,7 +12,7 @@
      -
    • +
    • Username or Email
    • @@ -22,7 +22,7 @@ role
    • -
    • @@ -46,7 +46,7 @@ {{user.login}} {{user.email}} - diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index aa844ee5113..b3f8a1ed164 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -60,6 +60,6 @@ function (angular, _, store, config) { store.set('grafana.sidemenu', false); } - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin') || this.hasRole('Read Only Editor'); + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); }); }); From 153ab4afaa13b5bf21f0ca88973cae60a170282b Mon Sep 17 00:00:00 2001 From: robert jakub Date: Mon, 1 Jun 2015 20:26:41 +0200 Subject: [PATCH 233/398] new role Read Only Editor - admin (small fix for #2088) --- public/app/features/admin/partials/edit_user.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 5d08292605e..a1a4cb989cd 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -115,7 +115,7 @@ Role
    • -
    • @@ -137,7 +137,7 @@ {{org.name}} Current - From 2446168356d6b6b4d16087ad7517ba0a15b588ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 09:00:17 +0200 Subject: [PATCH 234/398] Sort tags in search results, Closes #2091 --- pkg/search/handlers.go | 11 +++++++-- pkg/search/handlers_test.go | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 pkg/search/handlers_test.go diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index 874b85994ca..d0f9ae56833 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -55,12 +55,19 @@ func searchHandler(query *Query) error { hits = append(hits, jsonHits...) } - sort.Sort(hits) - + // add isStarred info if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { return err } + // sort main result array + sort.Sort(hits) + + // sort tags + for _, hit := range hits { + sort.Strings(hit.Tags) + } + query.Result = hits return nil } diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go new file mode 100644 index 00000000000..ebfade4dc06 --- /dev/null +++ b/pkg/search/handlers_test.go @@ -0,0 +1,49 @@ +package search + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestSearch(t *testing.T) { + + Convey("Given search query", t, func() { + jsonDashIndex = NewJsonDashIndex("../../public/dashboards/") + query := Query{} + + bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { + query.Result = HitList{ + &Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}}, + &Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}}, + &Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}}, + } + return nil + }) + + bus.AddHandler("test", func(query *m.GetUserStarsQuery) error { + query.Result = map[int64]bool{10: true, 12: true} + return nil + }) + + Convey("That is empty", func() { + err := searchHandler(&query) + So(err, ShouldBeNil) + + Convey("should return sorted results", func() { + So(query.Result[0].Title, ShouldEqual, "AABB") + So(query.Result[1].Title, ShouldEqual, "BBAA") + So(query.Result[2].Title, ShouldEqual, "CCAA") + }) + + Convey("should return sorted tags", func() { + So(query.Result[1].Tags[0], ShouldEqual, "AA") + So(query.Result[1].Tags[1], ShouldEqual, "BB") + So(query.Result[1].Tags[2], ShouldEqual, "EE") + }) + }) + + }) +} From dc607b8e8a8563e04cb09fa9b13f4397a486460a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 10:24:20 +0200 Subject: [PATCH 235/398] Dashboard search now supports filtering by multiple dashboard tags, Closes #2095 --- CHANGELOG.md | 1 + pkg/api/search.go | 4 +-- pkg/search/handlers.go | 41 +++++++++++++++++++++---- pkg/search/handlers_test.go | 12 ++++++++ pkg/search/json_index.go | 7 ----- pkg/search/json_index_test.go | 6 ++-- pkg/search/models.go | 3 +- pkg/services/sqlstore/dashboard.go | 7 +---- pkg/services/sqlstore/dashboard_test.go | 12 -------- public/app/controllers/search.js | 15 ++++++--- public/app/partials/search.html | 13 +++++--- 11 files changed, 74 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9096744be61..13b18a51fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior **Backend** +- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images diff --git a/pkg/api/search.go b/pkg/api/search.go index 10329f445bd..4c2ba9195b6 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -8,7 +8,7 @@ import ( func Search(c *middleware.Context) { query := c.Query("query") - tag := c.Query("tag") + tags := c.QueryStrings("tag") starred := c.Query("starred") limit := c.QueryInt("limit") @@ -18,7 +18,7 @@ func Search(c *middleware.Context) { searchQuery := search.Query{ Title: query, - Tag: tag, + Tags: tags, UserId: c.UserId, Limit: limit, IsStarred: starred == "true", diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index d0f9ae56833..3b4fcb1c59f 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -33,7 +33,6 @@ func searchHandler(query *Query) error { dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, - Tag: query.Tag, UserId: query.UserId, Limit: query.Limit, IsStarred: query.IsStarred, @@ -55,6 +54,22 @@ func searchHandler(query *Query) error { hits = append(hits, jsonHits...) } + // filter out results with tag filter + if len(query.Tags) > 0 { + filtered := HitList{} + for _, hit := range hits { + if hasRequiredTags(query.Tags, hit.Tags) { + filtered = append(filtered, hit) + } + } + hits = filtered + } + + // sort tags + for _, hit := range hits { + sort.Strings(hit.Tags) + } + // add isStarred info if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { return err @@ -63,15 +78,29 @@ func searchHandler(query *Query) error { // sort main result array sort.Sort(hits) - // sort tags - for _, hit := range hits { - sort.Strings(hit.Tags) - } - query.Result = hits return nil } +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func hasRequiredTags(queryTags, hitTags []string) bool { + for _, queryTag := range queryTags { + if !stringInSlice(queryTag, hitTags) { + return false + } + } + + return true +} + func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { query := m.GetUserStarsQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go index ebfade4dc06..193ba73f94a 100644 --- a/pkg/search/handlers_test.go +++ b/pkg/search/handlers_test.go @@ -45,5 +45,17 @@ func TestSearch(t *testing.T) { }) }) + Convey("That filters by tag", func() { + query.Tags = []string{"BB", "AA"} + err := searchHandler(&query) + So(err, ShouldBeNil) + + Convey("should return correct results", func() { + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Title, ShouldEqual, "BBAA") + So(query.Result[1].Title, ShouldEqual, "CCAA") + }) + + }) }) } diff --git a/pkg/search/json_index.go b/pkg/search/json_index.go index edf791562c9..a0fc02343e2 100644 --- a/pkg/search/json_index.go +++ b/pkg/search/json_index.go @@ -56,13 +56,6 @@ func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { break } - // filter out results with tag filter - if query.Tag != "" { - if !strings.Contains(item.TagsCsv, query.Tag) { - continue - } - } - // add results with matchig title filter if strings.Contains(item.TitleLower, query.Title) { results = append(results, &Hit{ diff --git a/pkg/search/json_index_test.go b/pkg/search/json_index_test.go index 52741cc6806..afd584fffbd 100644 --- a/pkg/search/json_index_test.go +++ b/pkg/search/json_index_test.go @@ -17,14 +17,14 @@ func TestJsonDashIndex(t *testing.T) { }) Convey("Should be able to search index", func() { - res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20}) + res, err := index.Search(&Query{Title: "", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 3) }) Convey("Should be able to search index by title", func() { - res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20}) + res, err := index.Search(&Query{Title: "home", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 1) @@ -32,7 +32,7 @@ func TestJsonDashIndex(t *testing.T) { }) Convey("Should not return when starred is filtered", func() { - res, err := index.Search(&Query{Title: "", Tag: "", IsStarred: true}) + res, err := index.Search(&Query{Title: "", IsStarred: true}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 0) diff --git a/pkg/search/models.go b/pkg/search/models.go index 157d9a292e3..e65428f14b0 100644 --- a/pkg/search/models.go +++ b/pkg/search/models.go @@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } type Query struct { Title string - Tag string + Tags []string OrgId int64 UserId int64 Limit int @@ -37,7 +37,6 @@ type Query struct { type FindPersistedDashboardsQuery struct { Title string - Tag string OrgId int64 UserId int64 Limit int diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 027f2cd2fac..8756c25e13e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -150,13 +150,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, "%"+query.Title+"%") } - if len(query.Tag) > 0 { - sql.WriteString(" AND dashboard_tag.term=?") - params = append(params, query.Tag) - } - if query.Limit == 0 || query.Limit > 10000 { - query.Limit = 300 + query.Limit = 1000 } sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit)) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 70675f9c42b..0ddd0db0df6 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) { So(len(hit.Tags), ShouldEqual, 2) }) - Convey("Should be able to search for dashboards using tags", func() { - query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1} - query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} - - err := SearchDashboards(&query1) - err = SearchDashboards(&query2) - So(err, ShouldBeNil) - - So(len(query1.Result), ShouldEqual, 1) - So(len(query2.Result), ShouldEqual, 0) - }) - Convey("Should not be able to save dashboard with same name", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, diff --git a/public/app/controllers/search.js b/public/app/controllers/search.js index b980297df16..a762af0f887 100644 --- a/public/app/controllers/search.js +++ b/public/app/controllers/search.js @@ -14,7 +14,7 @@ function (angular, _, config) { $scope.giveSearchFocus = 0; $scope.selectedIndex = -1; $scope.results = []; - $scope.query = { query: '', tag: '', starred: false }; + $scope.query = { query: '', tag: [], starred: false }; $scope.currentSearchId = 0; if ($scope.dashboardViewState.fullscreen) { @@ -82,12 +82,11 @@ function (angular, _, config) { $scope.queryHasNoFilters = function() { var query = $scope.query; - return query.query === '' && query.starred === false && query.tag === ''; + return query.query === '' && query.starred === false && query.tag.length === 0; }; $scope.filterByTag = function(tag, evt) { - $scope.query.tag = tag; - $scope.query.tagcloud = false; + $scope.query.tag.push(tag); $scope.search(); $scope.giveSearchFocus = $scope.giveSearchFocus + 1; if (evt) { @@ -96,6 +95,14 @@ function (angular, _, config) { } }; + $scope.removeTag = function(tag, evt) { + $scope.query.tag = _.without($scope.query.tag, tag); + $scope.search(); + $scope.giveSearchFocus = $scope.giveSearchFocus + 1; + evt.stopPropagation(); + evt.preventDefault(); + }; + $scope.getTags = function() { return backendSrv.get('/api/dashboards/tags').then(function(results) { $scope.tagsMode = true; diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 9c644e96d25..9eaa8d253d2 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -15,11 +15,14 @@ tags - - | - - {{query.tag}} - + + | + + + + {{tagName}} + +
    From 6df9012141f78f14213c6528ad131050c2bb928e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 10:35:10 +0200 Subject: [PATCH 236/398] Updated dashboard links feature to support search by my multiple tags, #1944 --- public/app/features/dashlinks/editor.html | 3 ++- public/app/features/dashlinks/module.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 576cde9c408..2d563c23c71 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -25,7 +25,8 @@
  • With tag
  • - + +
  • diff --git a/public/app/features/dashlinks/module.js b/public/app/features/dashlinks/module.js index e33d5406fc2..7be8ffbd87b 100644 --- a/public/app/features/dashlinks/module.js +++ b/public/app/features/dashlinks/module.js @@ -89,7 +89,7 @@ function (angular, _) { function buildLinks(linkDef) { if (linkDef.type === 'dashboards') { - if (!linkDef.tag) { + if (!linkDef.tags) { console.log('Dashboard link missing tag'); return $q.when([]); } @@ -97,7 +97,7 @@ function (angular, _) { if (linkDef.asDropdown) { return $q.when([{ title: linkDef.title, - tag: linkDef.tag, + tags: linkDef.tags, keepTime: linkDef.keepTime, includeVars: linkDef.includeVars, icon: "fa fa-bars", @@ -132,7 +132,7 @@ function (angular, _) { } $scope.searchDashboards = function(link) { - return backendSrv.search({tag: link.tag}).then(function(results) { + return backendSrv.search({tag: link.tags}).then(function(results) { return _.reduce(results, function(memo, dash) { // do not add current dashboard if (dash.id !== currentDashId) { From 50a1feb90a5535ff2ec4f6242e47e4e9209d7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 11:04:06 +0200 Subject: [PATCH 237/398] Dashboard list panel: Now supports search by multiple tags, Closes #2096 --- CHANGELOG.md | 1 + pkg/api/search.go | 2 +- pkg/search/handlers.go | 13 +++++++++---- pkg/search/handlers_test.go | 2 +- pkg/search/models.go | 1 - pkg/services/sqlstore/dashboard.go | 6 +----- public/app/directives/tags.js | 9 ++++++++- public/app/panels/dashlist/editor.html | 6 +++--- public/app/panels/dashlist/module.js | 7 +++++-- 9 files changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b18a51fcb..5988579123a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value - [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards - [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule +- [Issue #2096](https://github.com/grafana/grafana/issues/2096). Dashboard list panel: Now supports search by multiple tags **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). diff --git a/pkg/api/search.go b/pkg/api/search.go index 4c2ba9195b6..e451ac398d6 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -13,7 +13,7 @@ func Search(c *middleware.Context) { limit := c.QueryInt("limit") if limit == 0 { - limit = 200 + limit = 1000 } searchQuery := search.Query{ diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index 3b4fcb1c59f..a3cd01e2508 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -1,6 +1,7 @@ package search import ( + "fmt" "path/filepath" "sort" @@ -34,7 +35,6 @@ func searchHandler(query *Query) error { dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, UserId: query.UserId, - Limit: query.Limit, IsStarred: query.IsStarred, OrgId: query.OrgId, } @@ -65,6 +65,14 @@ func searchHandler(query *Query) error { hits = filtered } + // sort main result array + sort.Sort(hits) + + fmt.Printf("Length: %d", len(hits)) + if len(hits) > query.Limit { + hits = hits[0 : query.Limit-1] + } + // sort tags for _, hit := range hits { sort.Strings(hit.Tags) @@ -75,9 +83,6 @@ func searchHandler(query *Query) error { return err } - // sort main result array - sort.Sort(hits) - query.Result = hits return nil } diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go index 193ba73f94a..dc9835caa44 100644 --- a/pkg/search/handlers_test.go +++ b/pkg/search/handlers_test.go @@ -12,7 +12,7 @@ func TestSearch(t *testing.T) { Convey("Given search query", t, func() { jsonDashIndex = NewJsonDashIndex("../../public/dashboards/") - query := Query{} + query := Query{Limit: 2000} bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { query.Result = HitList{ diff --git a/pkg/search/models.go b/pkg/search/models.go index e65428f14b0..9b8c7627f89 100644 --- a/pkg/search/models.go +++ b/pkg/search/models.go @@ -39,7 +39,6 @@ type FindPersistedDashboardsQuery struct { Title string OrgId int64 UserId int64 - Limit int IsStarred bool Result HitList diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 8756c25e13e..01eacb8436a 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -150,11 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, "%"+query.Title+"%") } - if query.Limit == 0 || query.Limit > 10000 { - query.Limit = 1000 - } - - sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit)) + sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) var res []DashboardSearchProjection err := x.Sql(sql.String(), params...).Find(&res) diff --git a/public/app/directives/tags.js b/public/app/directives/tags.js index 4f8825a010b..f408a5e3864 100644 --- a/public/app/directives/tags.js +++ b/public/app/directives/tags.js @@ -70,7 +70,8 @@ function (angular, $) { return { restrict: 'EA', scope: { - model: '=ngModel' + model: '=ngModel', + onTagsUpdated: "&", }, template: '', replace: false, @@ -99,6 +100,9 @@ function (angular, $) { select.on('itemAdded', function(event) { if (scope.model.indexOf(event.item) === -1) { scope.model.push(event.item); + if (scope.onTagsUpdated) { + scope.onTagsUpdated(); + } } var tagElement = select.next().children("span").filter(function() { return $(this).text() === event.item; }); setColor(event.item, tagElement); @@ -108,6 +112,9 @@ function (angular, $) { var idx = scope.model.indexOf(event.item); if (idx !== -1) { scope.model.splice(idx, 1); + if (scope.onTagsUpdated) { + scope.onTagsUpdated(); + } } }); diff --git a/public/app/panels/dashlist/editor.html b/public/app/panels/dashlist/editor.html index 578d9e4b2d2..12598da9e56 100644 --- a/public/app/panels/dashlist/editor.html +++ b/public/app/panels/dashlist/editor.html @@ -27,11 +27,11 @@ ng-model="panel.query" ng-change="get_data()" ng-model-onblur>
  • - Tag + Tags
  • - + +
  • diff --git a/public/app/panels/dashlist/module.js b/public/app/panels/dashlist/module.js index da409fb3bde..9b4e1ebc045 100644 --- a/public/app/panels/dashlist/module.js +++ b/public/app/panels/dashlist/module.js @@ -32,7 +32,7 @@ function (angular, app, _, config, PanelMeta) { mode: 'starred', query: '', limit: 10, - tag: '', + tags: [] }; $scope.modes = ['starred', 'search']; @@ -43,6 +43,9 @@ function (angular, app, _, config, PanelMeta) { $scope.init = function() { panelSrv.init($scope); + if ($scope.panel.tag) { + $scope.panel.tags = [$scope.panel.tag]; + } if ($scope.isNewPanel()) { $scope.panel.title = "Starred Dashboards"; @@ -58,7 +61,7 @@ function (angular, app, _, config, PanelMeta) { params.starred = "true"; } else { params.query = $scope.panel.query; - params.tag = $scope.panel.tag; + params.tag = $scope.panel.tags; } return backendSrv.search(params).then(function(result) { From 8cfbd2f8bfd580c0e9489b45071b1c1e4ea66117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 11:05:48 +0200 Subject: [PATCH 238/398] Increased width of query input field in dashlist panel editor --- public/app/features/dashlinks/editor.html | 2 +- public/app/panels/dashlist/editor.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 2d563c23c71..69f2c067330 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -20,7 +20,7 @@
  • Type
  • - +
  • With tag
  • diff --git a/public/app/panels/dashlist/editor.html b/public/app/panels/dashlist/editor.html index 12598da9e56..ff2e75fd95c 100644 --- a/public/app/panels/dashlist/editor.html +++ b/public/app/panels/dashlist/editor.html @@ -23,7 +23,7 @@ Query
  • -
  • From bfe5a56a47ec6380127b2f0435ebfe470c922a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 12:52:31 +0200 Subject: [PATCH 239/398] Updated dashboard links editor, just changed With tag > With tags --- public/app/features/dashlinks/editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 69f2c067330..886550d9b9c 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -20,10 +20,10 @@
  • Type
  • - +
  • -
  • With tag
  • +
  • With tags
  • From 5c8b571c3f510c4cceeb0309c6b7291344acaa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 13:44:46 +0200 Subject: [PATCH 240/398] Fixed look of tag cloud in search --- public/app/partials/search.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 9eaa8d253d2..d4fc65afa38 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -33,7 +33,7 @@
    - + {{tag.term}}  ({{tag.count}}) From 7b69b789b8f8217236591a61bdbd396f6ae8fc26 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Tue, 2 Jun 2015 16:45:44 +0100 Subject: [PATCH 241/398] Fixed typos --- README.md | 2 +- docs/sources/datasources/opentsdb.md | 2 +- docs/sources/guides/gettingstarted.md | 4 ++-- docs/sources/installation/configuration.md | 2 +- docs/sources/project/building_from_source.md | 2 +- docs/sources/reference/annotations.md | 4 ++-- docs/sources/reference/graph.md | 2 +- docs/sources/reference/http_api.md | 4 ++-- docs/sources/reference/scripting.md | 2 +- docs/sources/reference/timerange.md | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5f0fb88b951..0a7db318338 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ go get github.com/grafana/grafana ``` cd $GOPATH/src/github.com/grafana/grafana go run build.go setup (only needed once to install godep) -godep restore (will pull down all golang lib dependecies in your current GOPATH) +godep restore (will pull down all golang lib dependencies in your current GOPATH) go build . ``` diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index 09b9f647a5f..4e85bdc9555 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -27,7 +27,7 @@ Open a graph in edit mode by click the title. ![](/img/v2/opentsdb_query_editor.png) -For details on opentsdb metric queries checkout the offical [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) +For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index b0037668f56..641dc9516f5 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -19,7 +19,7 @@ The image above shows you the top header for a dashboard. 1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources. 2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists. -3. Star Dashboard: Star (or unstar) the current Dashboar. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in. +3. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in. 4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing. 5. Save dashboard: The current Dashboard will be saved with the current Dashboard name. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations. @@ -28,7 +28,7 @@ The image above shows you the top header for a dashboard. Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. By adjusting the display properties of Panels and Rows, you can customize the perfect Dashboard for your exact needs. Each panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). -This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specificed +This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specified in the main Time Picker in the upper right, but they can also have relative time overrides. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index e27a3e80f6a..80ad5909a34 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -301,7 +301,7 @@ Secret. Specify these in the Grafana configuration file. For example: Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains were seperated by space. +accounts. The `allowed_domains` option is optional, and domains were separated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 8466dbd2736..8401bf543de 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -72,4 +72,4 @@ You only need to add the options you want to override. Config files are applied ## Create a pull requests -Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html). +Before or after your create a pull requests, sign the [contributor license agreement](/docs/contributing/cla.html). diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index 41d7bf411a3..51c9ac1ba32 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -18,9 +18,9 @@ dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add Graphite supports two ways to query annotations. - A regular metric query, use the `Graphite target expression` text input for this -- Graphite events query, use the `Graphite event tags` text input, especify an tag or wildcard (leave empty should also work) +- Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work) -## Elasticsearch annoations +## Elasticsearch annotations ![](/img/v2/annotations_es.png) Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern. diff --git a/docs/sources/reference/graph.md b/docs/sources/reference/graph.md index 61bebd6b37b..9de23332a99 100644 --- a/docs/sources/reference/graph.md +++ b/docs/sources/reference/graph.md @@ -62,7 +62,7 @@ The ``Left Y`` and ``Right Y`` can be customized using: - ``Unit`` - The display unit for the Y value - ``Grid Max`` - The maximum Y value. (default auto) -- ``Grid Min`` - The minium Y value. (default auto) +- ``Grid Min`` - The minimum Y value. (default auto) - ``Label`` - The Y axis label (default "") Axes can also be hidden by unchecking the appropriate box from `Show Axis`. diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index d888071bd35..9e24ccb39d7 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -84,8 +84,8 @@ Status Codes: - **401** – Unauthorized - **412** – Precondition failed -The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The -same status code is also used if another dashboar exists with the same title. The response body will look like this: +The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The +same status code is also used if another dashboard exists with the same title. The response body will look like this: HTTP/1.1 412 Precondition Failed Content-Type: application/json; charset=UTF-8 diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 45158eeadcd..d896a2c7650 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -12,7 +12,7 @@ With scripted dashboards you can dynamically create your dashboards using javasc under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` -If you open scripted.js you can see how it reads url paramters from ARGS variable and then adds rows and panels. +If you open scripted.js you can see how it reads url parameters from ARGS variable and then adds rows and panels. ## Example diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md index 47c9186e119..d2f05f5a9d6 100644 --- a/docs/sources/reference/timerange.md +++ b/docs/sources/reference/timerange.md @@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time It's possible to customize the options displayed for relative time and the auto-refresh options. -From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma seperated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years). +From Dashboard settings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma separated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years). ![](/img/v1/timepicker_editor.png) From f582ac88b382f72f102b43aeab33a167454590c7 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Tue, 2 Jun 2015 17:12:12 +0100 Subject: [PATCH 242/398] Fixed menu --- docs/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2ff90577f07..5024fd33afe 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -61,7 +61,7 @@ pages: - ['datasources/influxdb.md', 'Data Sources', 'InfluxDB'] - ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB'] -- ['project/building_from_source.md', 'Project', 'Building from souce'] +- ['project/building_from_source.md', 'Project', 'Building from source'] - ['project/cla.md', 'Project', 'Contributor License Agreement'] - ['jsearch.md', '**HIDDEN**'] From 483ef20527a2168628342b22b39fadbe1d6f60e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:16:16 +0200 Subject: [PATCH 243/398] Reworking variable value dropdown, take3 --- public/app/directives/metric.segment.js | 2 +- public/app/directives/variableValueSelect.js | 96 ++++++++++++------- .../partials/variableValueSelect.html | 39 ++++---- .../features/templating/partials/editor.html | 70 +++++++------- public/css/less/submenu.less | 31 +++--- public/css/less/tightform.less | 3 +- 6 files changed, 133 insertions(+), 108 deletions(-) diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index 05bf7e485e3..4f5677ca3ed 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -68,7 +68,7 @@ function (angular, app, _, $) { else { // need to have long delay because the blur // happens long before the click event on the typeahead options - cancelBlur = setTimeout($scope.switchToLink, 350); + cancelBlur = setTimeout($scope.switchToLink, 50); } }; diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index fa0e56acebc..42724a79df9 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -10,23 +10,72 @@ function (angular, app, _) { angular .module('grafana.directives') .directive('variableValueSelect', function($compile, $window, $timeout) { + + function openDropdown(inputEl, linkEl) { + inputEl.css('width', (linkEl.width() + 16) + 'px'); + + linkEl.hide(); + inputEl.show(); + inputEl.focus(); + }; + return { scope: { variable: "=", onUpdated: "&" }, + templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', + link: function(scope, elem) { var bodyEl = angular.element($window.document.body); + var linkEl = elem.find('.variable-value-link'); + var inputEl = elem.find('input'); var variable = scope.variable; + var cancelBlur = null; - scope.show = function() { - if (scope.selectorOpen) { - return; + scope.openDropdown = function() { + inputEl.show(); + linkEl.hide(); + scope.dropdownVisible = true; + + inputEl.css('width', (linkEl.width() + 16) + 'px'); + + linkEl.hide(); + inputEl.show(); + inputEl.focus(); + + $timeout(function() { bodyEl.on('click', scope.bodyOnClick); }, 0, false); + }; + + scope.switchToLink = function(now) { + if (now === true || cancelBlur) { + clearTimeout(cancelBlur); + cancelBlur = null; + inputEl.hide(); + linkEl.show(); + scope.dropdownVisible = false; + scope.$digest(); + + scope.updateLinkText(); + scope.onUpdated(); + } + else { + // need to have long delay because the blur + // happens long before the click event on the typeahead options + cancelBlur = setTimeout(scope.switchToLink, 50); } - scope.selectorOpen = true; - scope.giveFocus = 1; + bodyEl.off('click', scope.bodyOnClick); + }; + + scope.bodyOnClick = function(e) { + if (elem.has(e.target).length === 0) { + scope.switchToLink(); + } + }; + + scope.show = function() { scope.oldCurrentText = variable.current.text; scope.highlightIndex = -1; @@ -45,9 +94,7 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; - $timeout(function() { - bodyEl.on('click', scope.bodyOnClick); - }, 0, false); + scope.openDropdown(); }; scope.queryChanged = function() { @@ -79,7 +126,7 @@ function (angular, app, _) { scope.optionSelected = function(option, event) { option.selected = !option.selected; - var hideAfter = true; + var hideAfter = false; var setAllExceptCurrentTo = function(newValue) { _.each(scope.options, function(other) { if (option !== other) { other.selected = newValue; } @@ -91,13 +138,10 @@ function (angular, app, _) { } else if (!variable.multi) { setAllExceptCurrentTo(false); - } else { - if (event.ctrlKey || event.metaKey || event.shiftKey) { - hideAfter = false; - } - else { - setAllExceptCurrentTo(false); - } + hideAfter = true; + } else if (event.ctrlKey || event.metaKey || event.shiftKey) { + hideAfter = true; + setAllExceptCurrentTo(false); } var selected = _.filter(scope.options, {selected: true}); @@ -124,23 +168,8 @@ function (angular, app, _) { variable.current.value = selected[0].value; } - scope.updateLinkText(); - scope.onUpdated(); - if (hideAfter) { - scope.hide(); - } - }; - - 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.switchToLink(); } }; @@ -152,6 +181,9 @@ function (angular, app, _) { scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() { scope.updateLinkText(); }); + + linkEl.click(scope.openDropdown); + //inputEl.blur(scope.switchToLink); }, }; }); diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 481f2734876..afa4bdbe649 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,35 +1,28 @@ - + {{labelText}}:
    - + {{linkText}} + -
    -
    - - - + diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index d1232c72dcf..99b50d45866 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -226,41 +226,41 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    +
    +
    value groups/tags
    +
    +
      +
    • + tags query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + tags values query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index 4fefa50d5e7..a92cb163537 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -20,7 +20,7 @@ } .submenu-item { - padding: 8px 7px; +// padding: 8px 7px; margin-right: 20px; display: inline-block; border-radius: 3px; @@ -43,10 +43,10 @@ .variable-value-dropdown { position: absolute; - top: 27px; + top: 47px; min-width: 150px; max-height: 400px; - background: @grafanaPanelBackground; + background: @dropdownBackground; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; @@ -74,22 +74,23 @@ .variable-option, .variable-options-column-header { display: block; - padding: 0 27px 0 8px; + padding: 2px 27px 0 8px; position: relative; + white-space: nowrap; + min-width: 115px; - .variable-option-icon { display: none } + .variable-option-icon { + display: inline-block; + width: 24px; + height: 18px; + position: relative; + top: 4px; + background: url(@checkboxImageUrl) left top no-repeat; + } &.selected { - .variable-option-icon:before { - content: "\f00c"; - } - .variable-option-icon { - display: block; - padding-left: 4px; - line-height: 26px; - position: absolute; - right: 0; - top: 0; + .variable-option-icon{ + background: url(@checkboxImageUrl) 0px -18px no-repeat; } } } diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index 5bd9cda8f43..71457a62141 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -23,7 +23,7 @@ .tight-form-container-no-item-borders { border: 1px solid @grafanaTargetBorder; - .tight-form, .tight-form-item, [type=text].tight-form-input { + .tight-form, .tight-form-item, [type=text].tight-form-input, [type=text].tight-form-clear-input { border: none; } } @@ -132,7 +132,6 @@ input[type=text].tight-form-clear-input { border: none; margin: 0px; background: transparent; - float: left; color: @grafanaTargetColor; border-radius: 0; border-right: 1px solid @grafanaTargetSegmentBorder; From a433e0e79c10a74264b0d456f5596ef7c5092180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:39:08 +0200 Subject: [PATCH 244/398] More work on variable dropdown --- public/app/directives/variableValueSelect.js | 27 +++++++++++++++----- public/app/features/templating/editorCtrl.js | 1 + 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 42724a79df9..43c1014160e 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -115,7 +115,10 @@ function (angular, app, _) { scope.moveHighlight(-1); } if (evt.keyCode === 13) { - scope.optionSelected(scope.search.options[scope.highlightIndex], {}); + scope.optionSelected(scope.search.options[scope.highlightIndex], {}, true, false); + } + if (evt.keyCode === 32) { + scope.optionSelected(scope.search.options[scope.highlightIndex], {}, false, false); } }; @@ -123,24 +126,34 @@ function (angular, app, _) { scope.highlightIndex = (scope.highlightIndex + direction) % scope.search.options.length; }; - scope.optionSelected = function(option, event) { + scope.optionSelected = function(option, event, commitChange, excludeOthers) { + if (!option) { return; } + option.selected = !option.selected; - var hideAfter = false; + commitChange = commitChange || false; + excludeOthers = excludeOthers || false; + var setAllExceptCurrentTo = function(newValue) { _.each(scope.options, function(other) { if (option !== other) { other.selected = newValue; } }); }; - if (option.text === 'All') { + // commit action (enter key), should not deselect it + if (commitChange) { + option.selected = true; + } + + if (option.text === 'All' || excludeOthers) { setAllExceptCurrentTo(false); + commitChange = true; } else if (!variable.multi) { setAllExceptCurrentTo(false); - hideAfter = true; + commitChange = true; } else if (event.ctrlKey || event.metaKey || event.shiftKey) { - hideAfter = true; + commitChange = true; setAllExceptCurrentTo(false); } @@ -168,7 +181,7 @@ function (angular, app, _) { variable.current.value = selected[0].value; } - if (hideAfter) { + if (commitChange) { scope.switchToLink(); } }; diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index f48452e4569..aeb27b3c832 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -82,6 +82,7 @@ function (angular, _) { }; $scope.update = function() { + $scope.current.tags = []; if ($scope.isValid()) { $scope.runQuery().then(function() { $scope.reset(); From 9a741051037398f3937fe646694fd8d4709d24c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 12:52:47 +0200 Subject: [PATCH 245/398] Trying out dark headers and footers for template dropdown --- public/app/directives/variableValueSelect.js | 5 ++- .../partials/variableValueSelect.html | 32 ++++++++++++------- public/css/less/submenu.less | 29 ++++++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 43c1014160e..e74564369c2 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -93,6 +93,7 @@ function (angular, app, _) { }); scope.search = {query: '', options: scope.options}; + scope.selectedValuesCount = currentValues.length; scope.openDropdown(); }; @@ -176,8 +177,10 @@ function (angular, app, _) { value: _.pluck(selected, 'value'), }; + scope.selectedValuesCount = variable.current.value.length; + // only single value - if (variable.current.value.length === 1) { + if (scope.selectedValuesCount === 1) { variable.current.value = selected[0].value; } diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index afa4bdbe649..3bbc5060c0e 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -10,19 +10,27 @@
    -
    - - - - {{option.text}} - +
    +
    +
    + Selected ({{selectedValuesCount}}) +
    + + + {{option.text}} + +
    +
    -
    - - - - {{tag}}    - +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index a92cb163537..de12ca4a108 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -46,7 +46,10 @@ top: 47px; min-width: 150px; max-height: 400px; + min-height: 150px; background: @dropdownBackground; + overflow-y: auto; + overflow-x: hidden; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; @@ -62,7 +65,6 @@ .variable-options-column { max-height: 350px; - overflow: auto; display: table-cell; line-height: 26px; &:nth-child(2) { @@ -95,22 +97,27 @@ } } +.variable-options-column-header { + background-color: @bodyBackground; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 5px; +} + +.variable-options-footer { + background-color: @bodyBackground; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; +} + .variable-option { &:hover, &.highlighted { background-color: @blueDark; } } -.variable-search-wrapper { - input { - width: 100%; - padding: 7px 8px; - height: 100%; - box-sizing: border-box; - margin-bottom: 6px; - } -} - .dash-nav-link { color: @textColor; } From 0bd50c06d77015fa9826e6f082daac9b488b9f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 13:02:01 +0200 Subject: [PATCH 246/398] Template varaible dropdown work --- .../features/dashboard/partials/variableValueSelect.html | 4 ---- public/css/less/submenu.less | 8 -------- 2 files changed, 12 deletions(-) diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 3bbc5060c0e..de22c9cc54e 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -28,9 +28,5 @@
    -
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index de12ca4a108..cbdff413a0b 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -98,20 +98,12 @@ } .variable-options-column-header { - background-color: @bodyBackground; text-align: center; padding-top: 5px; padding-bottom: 5px; margin-bottom: 5px; } -.variable-options-footer { - background-color: @bodyBackground; - text-align: center; - padding-top: 5px; - padding-bottom: 5px; -} - .variable-option { &:hover, &.highlighted { background-color: @blueDark; From 7d25d6f1915e56477504416584ccdf8f48ad5d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 18:38:53 +0200 Subject: [PATCH 247/398] progress on tag selection in variable dropdown --- public/app/directives/variableValueSelect.js | 47 ++++++++++++++++++- .../partials/variableValueSelect.html | 8 +++- public/app/features/templating/editorCtrl.js | 1 - public/css/less/overrides.less | 2 +- public/css/less/submenu.less | 3 ++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index e74564369c2..c22154f68a6 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -95,6 +95,12 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; scope.selectedValuesCount = currentValues.length; + if (!scope.tags) { + scope.tags = _.map(variable.tags, function(value) { + return { text: value, selected: false }; + }); + } + scope.openDropdown(); }; @@ -158,11 +164,15 @@ function (angular, app, _) { setAllExceptCurrentTo(false); } + scope.selectionsChanged(option, commitChange); + }; + + scope.selectionsChanged = function(defaultItem, commitChange) { var selected = _.filter(scope.options, {selected: true}); if (selected.length === 0) { - option.selected = true; - selected = [option]; + defaultItem.selected = true; + selected = [defaultItem]; } if (selected.length > 1 && selected.length !== scope.options.length) { @@ -177,6 +187,18 @@ function (angular, app, _) { value: _.pluck(selected, 'value'), }; + var valuesNotInTag = _.filter(selected, function(test) { + for (var i = 0; i < scope.selectedTags.length; i++) { + var tag = scope.selectedTags[i]; + if (_.indexOf(tag.values, test.value) !== -1) { + return false; + } + } + return true; + }); + + variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); + scope.selectedValuesCount = variable.current.value.length; // only single value @@ -189,6 +211,27 @@ function (angular, app, _) { } }; + scope.selectTag = function(tag) { + tag.selected = !tag.selected; + if (!tag.values) { + if (tag.text === 'backend') { + tag.values = ['backend_01', 'backend_02', 'backend_03', 'backend_04']; + } else { + tag.values = ['web_server_01', 'web_server_02', 'web_server_03', 'web_server_04']; + } + console.log('querying for tag values'); + } + + _.each(scope.options, function(option) { + if (_.indexOf(tag.values, option.value) !== -1) { + option.selected = tag.selected; + } + }); + + scope.selectedTags = _.filter(scope.tags, {selected: true}); + scope.selectionsChanged(scope.options[0], false); + }; + scope.updateLinkText = function() { scope.labelText = variable.label || '$' + variable.name; scope.linkText = variable.current.text; diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index de22c9cc54e..bb4f0ac03e6 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -5,6 +5,10 @@
    diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index aeb27b3c832..f48452e4569 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -82,7 +82,6 @@ function (angular, _) { }; $scope.update = function() { - $scope.current.tags = []; if ($scope.isValid()) { $scope.runQuery().then(function() { $scope.reset(); diff --git a/public/css/less/overrides.less b/public/css/less/overrides.less index b03cb7f7e6b..6ac6a385b60 100644 --- a/public/css/less/overrides.less +++ b/public/css/less/overrides.less @@ -542,7 +542,7 @@ div.flot-text { background-color: @purple; color: darken(@white, 5%); white-space: nowrap; - border-radius: 2px; + border-radius: 3px; text-shadow: none; font-size: 13px; padding: 2px 6px; diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index cbdff413a0b..e2d6b0639dd 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -39,6 +39,9 @@ .variable-value-link { font-size: 16px; padding-right: 10px; + .label-tag { + margin: 0 5px; + } } .variable-value-dropdown { From 6ed17fe62fe692a9a9961849b4c19653fc8efc3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 29 May 2015 08:21:44 +0200 Subject: [PATCH 248/398] Removed selection state for single select variables --- public/app/directives/variableValueSelect.js | 15 ++------ .../partials/variableValueSelect.html | 9 ++--- public/css/less/submenu.less | 34 +++++++++++-------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index c22154f68a6..f51c706bfe5 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -11,20 +11,8 @@ function (angular, app, _) { .module('grafana.directives') .directive('variableValueSelect', function($compile, $window, $timeout) { - function openDropdown(inputEl, linkEl) { - inputEl.css('width', (linkEl.width() + 16) + 'px'); - - linkEl.hide(); - inputEl.show(); - inputEl.focus(); - }; - return { - scope: { - variable: "=", - onUpdated: "&" - }, - + scope: { variable: "=", onUpdated: "&" }, templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', link: function(scope, elem) { @@ -94,6 +82,7 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; scope.selectedValuesCount = currentValues.length; + scope.selectedTags = scope.selectedTag || []; if (!scope.tags) { scope.tags = _.map(variable.tags, function(value) { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index bb4f0ac03e6..303e1756033 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -2,7 +2,7 @@ {{labelText}}: -
    +
  • diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index bd9f0919d20..c919b3b8bee 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -8,11 +8,17 @@ function () { describe("SelectDropdownCtrl", function() { var scope; var ctrl; + var tagValuesMap = {}; + var rootScope; beforeEach(module('grafana.controllers')); - beforeEach(inject(function($controller, $rootScope) { + beforeEach(inject(function($controller, $rootScope, $q) { + rootScope = $rootScope; scope = $rootScope.$new(); ctrl = $controller('SelectDropdownCtrl', {$scope: scope}); + ctrl.getValuesForTag = function(obj) { + return $q.when(tagValuesMap[obj.tagKey]); + }; })); describe("Given simple variable", function() { @@ -24,9 +30,71 @@ function () { it("Should init labelText and linkText", function() { expect(ctrl.linkText).to.be("hej"); }); - }); - }); + describe("Given variable with tags and dropdown is opened", function() { + beforeEach(function() { + ctrl.variable = { + current: {text: 'hej', value: 'hej'}, + options: [ + {text: 'server-1', value: 'server-1'}, + {text: 'server-2', value: 'server-2'}, + {text: 'server-3', value: 'server-3'}, + ], + tags: ["key1", "key2", "key3"] + }; + tagValuesMap.key1 = ['server-1', 'server-3']; + tagValuesMap.key2 = ['server-2', 'server-3']; + tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; + ctrl.init(); + ctrl.show(); + }); + it("should init tags model", function() { + expect(ctrl.tags.length).to.be(3); + expect(ctrl.tags[0].text).to.be("key1"); + }); + + it("should init options model", function() { + expect(ctrl.options.length).to.be(3); + }); + + describe('When tag is selected', function() { + beforeEach(function() { + ctrl.selectTag(ctrl.tags[0]); + rootScope.$digest(); + }); + + it("should select tag", function() { + expect(ctrl.selectedTags.length).to.be(1); + }); + + it("should select values", function() { + expect(ctrl.options[0].selected).to.be(true); + expect(ctrl.options[2].selected).to.be(true); + }); + + describe('and then unselected', function() { + beforeEach(function() { + ctrl.selectTag(ctrl.tags[0]); + rootScope.$digest(); + }); + + it("should deselect tag", function() { + expect(ctrl.selectedTags.length).to.be(0); + }); + }); + + describe('and then value is unselected', function() { + beforeEach(function() { + ctrl.optionSelected(ctrl.options[0]); + }); + + it("should deselect tag", function() { + expect(ctrl.selectedTags.length).to.be(0); + }); + }); + }); + }); + }); }); From 650d3d504687bfb4934890a96017f0bef8eddf05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 29 May 2015 17:51:07 +0200 Subject: [PATCH 254/398] Fixed tag selection issues --- public/app/directives/variableValueSelect.js | 6 ++---- public/app/features/templating/templateValuesSrv.js | 2 +- public/test/specs/selectDropdownCtrl-specs.js | 12 ++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index f77510afc0d..41f8d5b1322 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -31,7 +31,7 @@ function (angular, app, _) { vm.search = {query: '', options: vm.options}; vm.selectedValuesCount = currentValues.length; - vm.selectedTags = vm.selectedTag || []; + vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { vm.tags = _.map(vm.variable.tags, function(value) { @@ -76,7 +76,6 @@ function (angular, app, _) { } }); - vm.selectedTags = _.filter(vm.tags, {selected: true}); vm.selectionsChanged(false); }); }; @@ -168,10 +167,9 @@ function (angular, app, _) { return true; }); - vm.variable.current = {}; vm.variable.current.value = _.pluck(selected, 'value'); vm.variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); - vm.selectedValuesCount = vm.variable.current.value.length; + vm.selectedValuesCount = selected.length; // only single value if (vm.selectedValuesCount === 1) { diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 7330be1b5cd..2fd17f4aa65 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -78,7 +78,7 @@ function (angular, _, kbn) { }; this.setVariableValue = function(variable, option) { - variable.current = option; + variable.current = angular.copy(option); templateSrv.updateTemplateData(); return this.updateOptionsInChildVariables(variable); }; diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index c919b3b8bee..968320b8e4f 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -74,6 +74,18 @@ function () { expect(ctrl.options[2].selected).to.be(true); }); + describe('and then dropdown is opened and closed without changes', function() { + beforeEach(function() { + ctrl.show(); + ctrl.commitChanges(); + rootScope.$digest(); + }); + + it("should still have selected tag", function() { + expect(ctrl.selectedTags.length).to.be(1); + }); + }); + describe('and then unselected', function() { beforeEach(function() { ctrl.selectTag(ctrl.tags[0]); From f48d0fcb13b193e5006b2bb54b7a6877eb7fa27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 30 May 2015 09:34:11 +0200 Subject: [PATCH 255/398] More work on tags in variable dropdown, now the actual tag values query is hooked up and works, #2080 --- public/app/directives/variableValueSelect.js | 5 ++++- public/app/features/dashboard/submenuCtrl.js | 8 +++----- .../features/templating/partials/editor.html | 20 +++++++++---------- .../features/templating/templateValuesSrv.js | 11 ++++++++++ public/app/partials/submenu.html | 2 +- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 41f8d5b1322..bdc914c144d 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -13,7 +13,7 @@ function (angular, app, _) { var vm = this; vm.show = function() { - vm.oldCurrentText = vm.variable.current.text; + vm.oldLinkText = vm.variable.current.text; vm.highlightIndex = -1; var currentValues = vm.variable.current.value; @@ -44,6 +44,9 @@ function (angular, app, _) { vm.updateLinkText = function() { vm.linkText = vm.variable.current.text; + if (vm.oldLinkText && vm.oldLinkText !== vm.linkText) { + vm.onUpdated(); + } }; vm.clearSelections = function() { diff --git a/public/app/features/dashboard/submenuCtrl.js b/public/app/features/dashboard/submenuCtrl.js index 6f423ebbe59..b8e609c061e 100644 --- a/public/app/features/dashboard/submenuCtrl.js +++ b/public/app/features/dashboard/submenuCtrl.js @@ -18,9 +18,7 @@ function (angular, _) { $scope.panel = $scope.pulldown; $scope.row = $scope.pulldown; $scope.annotations = $scope.dashboard.templating.list; - $scope.variables = _.map($scope.dashboard.templating.list, function(variable) { - return variable; - }); + $scope.variables = $scope.dashboard.templating.list; }; $scope.disableAnnotation = function (annotation) { @@ -28,8 +26,8 @@ function (angular, _) { $rootScope.$broadcast('refresh'); }; - $scope.getValuesForTag = function() { - return $q.when(['backend_01', 'backend_02']); + $scope.getValuesForTag = function(variable, tagKey) { + return templateValuesSrv.getValuesForTag(variable, tagKey); }; $scope.variableUpdated = function(variable) { diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 99b50d45866..382790e0f21 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -228,25 +228,25 @@
    -
    value groups/tags
    -
    +
    Value groups/tags
    +
      -
    • - tags query +
    • + Tags query
    • - +
    -
    +
      -
    • - tags values query +
    • + Tag values query
    • - +
    @@ -254,7 +254,7 @@
    • - +
    diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 2fd17f4aa65..8b61b40688d 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -154,6 +154,17 @@ function (angular, _, kbn) { }); }; + this.getValuesForTag = function(variable, tagKey) { + return datasourceSrv.get(variable.datasource).then(function(datasource) { + var query = variable.tagValuesQuery.replace('$tag', tagKey); + return datasource.metricFindQuery(query).then(function (results) { + return _.map(results, function(value) { + return value.text; + }); + }); + }); + }; + this.metricNamesToVariableValues = function(variable, metricNames) { var regex, options, i, matches; options = {}; // use object hash to remove duplicates diff --git a/public/app/partials/submenu.html b/public/app/partials/submenu.html index 43a5581e0b5..5f5d29e87ba 100644 --- a/public/app/partials/submenu.html +++ b/public/app/partials/submenu.html @@ -6,7 +6,7 @@ {{variable.label || variable.name}}: - + From b5a846154a3ca821c4f781591987c2cc295e7c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 10:43:25 +0200 Subject: [PATCH 256/398] Trying to make progres on persisting selection state, restoring selection state for new multi variable dropdown, proving to be really complex --- public/app/directives/variableValueSelect.js | 88 +++++++++---------- .../partials/variableValueSelect.html | 4 +- .../features/templating/templateValuesSrv.js | 4 + public/test/specs/selectDropdownCtrl-specs.js | 33 ++++++- 4 files changed, 79 insertions(+), 50 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index bdc914c144d..0c3ff0e3507 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -13,24 +13,20 @@ function (angular, app, _) { var vm = this; vm.show = function() { - vm.oldLinkText = vm.variable.current.text; + vm.oldVariableText = vm.variable.current.text; vm.highlightIndex = -1; var currentValues = vm.variable.current.value; - if (_.isString(currentValues)) { currentValues = [currentValues]; } vm.options = _.map(vm.variable.options, function(option) { - if (_.indexOf(currentValues, option.value) >= 0) { - option.selected = true; - } + if (_.indexOf(currentValues, option.value) >= 0) { option.selected = true; } return option; }); - vm.search = {query: '', options: vm.options}; - vm.selectedValuesCount = currentValues.length; + vm.selectedValues = _.filter(vm.options, {selected: true}); vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { @@ -39,14 +35,26 @@ function (angular, app, _) { }); } + vm.search = {query: '', options: vm.options}; vm.dropdownVisible = true; }; vm.updateLinkText = function() { + // var currentValues = vm.variable.current.text; + // + // if (vm.variable.current.tags) { + // selectedOptions = _.filter(selectedOptions, function(test) { + // for (var i = 0; i < vm.variable.current.tags; i++) { + // var tag = vm.selectedTags[i]; + // if (_.indexOf(tag.values, test.text) !== -1) { + // return false; + // } + // } + // return true; + // }); + // } + // vm.linkText = vm.variable.current.text; - if (vm.oldLinkText && vm.oldLinkText !== vm.linkText) { - vm.onUpdated(); - } }; vm.clearSelections = function() { @@ -62,17 +70,13 @@ function (angular, app, _) { var tagValuesPromise; if (!tag.values) { tagValuesPromise = vm.getValuesForTag({tagKey: tag.text}); - // if (tag.text === 'backend') { - // tag.values = ['backend_01', 'backend_02', 'backend_03', 'backend_04']; - // } else { - // tag.values = ['web_server_01', 'web_server_02', 'web_server_03', 'web_server_04']; - // } } else { tagValuesPromise = $q.when(tag.values); } tagValuesPromise.then(function(values) { tag.values = values; + tag.valuesText = values.join(', '); _.each(vm.options, function(option) { if (_.indexOf(tag.values, option.value) !== -1) { option.selected = tag.selected; @@ -105,7 +109,7 @@ function (angular, app, _) { vm.highlightIndex = (vm.highlightIndex + direction) % vm.search.options.length; }; - vm.optionSelected = function(option, event, commitChange, excludeOthers) { + vm.selectValue = function(option, event, commitChange, excludeOthers) { if (!option) { return; } option.selected = !option.selected; @@ -140,43 +144,34 @@ function (angular, app, _) { }; vm.selectionsChanged = function(commitChange) { - var selected = _.filter(vm.options, {selected: true}); + vm.selectedValues = _.filter(vm.options, {selected: true}); - if (selected.length > 1 && selected.length !== vm.options.length) { - if (selected[0].text === 'All') { - selected[0].selected = false; - selected = selected.slice(1, selected.length); + if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) { + if (vm.selectedValues[0].text === 'All') { + vm.selectedValues[0].selected = false; + vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length); } } // validate selected tags - _.each(vm.selectedTags, function(tag) { - _.each(tag.values, function(value) { - if (!_.findWhere(selected, {value: value})) { - tag.selected = false; - } - }); + _.each(vm.tags, function(tag) { + if (tag.selected) { + _.each(tag.values, function(value) { + if (!_.findWhere(vm.selectedValues, {value: value})) { + tag.selected = false; + } + }); + } }); vm.selectedTags = _.filter(vm.tags, {selected: true}); - - var valuesNotInTag = _.filter(selected, function(test) { - for (var i = 0; i < vm.selectedTags.length; i++) { - var tag = vm.selectedTags[i]; - if (_.indexOf(tag.values, test.value) !== -1) { - return false; - } - } - return true; - }); - - vm.variable.current.value = _.pluck(selected, 'value'); - vm.variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); - vm.selectedValuesCount = selected.length; + vm.variable.current.value = _.pluck(vm.selectedValues, 'value'); + vm.variable.current.text = _.pluck(vm.selectedValues, 'text').join(' + '); + vm.variable.current.tags = vm.selectedTags; // only single value - if (vm.selectedValuesCount === 1) { - vm.variable.current.value = selected[0].value; + if (vm.selectedValues.length === 1) { + vm.variable.current.value = vm.selectedValues[0].value; } if (commitChange) { @@ -186,14 +181,17 @@ function (angular, app, _) { vm.commitChanges = function() { // make sure one option is selected - var selected = _.filter(vm.options, {selected: true}); - if (selected.length === 0) { + if (vm.selectedValues.length === 0) { vm.options[0].selected = true; vm.selectionsChanged(false); } vm.dropdownVisible = false; vm.updateLinkText(); + + if (vm.variable.current.text !== vm.oldVariableText) { + vm.onUpdated(); + } }; vm.queryChanged = function() { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 169e868337b..011522d6afe 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -15,9 +15,9 @@
    - Selected ({{vm.selectedValuesCount}}) + Selected ({{vm.selectedValues.length}}) - + {{option.text}} diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 8b61b40688d..1cfa9bfc4ad 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -130,6 +130,10 @@ function (angular, _, kbn) { // if parameter has current value // if it exists in options array keep value if (variable.current) { + // if current value is an array do not do anything + if (_.isArray(variable.current.value)) { + return $q.when([]); + } var currentOption = _.findWhere(variable.options, { text: variable.current.text }); if (currentOption) { return self.setVariableValue(variable, currentOption); diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index 968320b8e4f..603acc9a5fb 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -19,6 +19,7 @@ function () { ctrl.getValuesForTag = function(obj) { return $q.when(tagValuesMap[obj.tagKey]); }; + ctrl.onUpdated = sinon.spy(); })); describe("Given simple variable", function() { @@ -35,13 +36,14 @@ function () { describe("Given variable with tags and dropdown is opened", function() { beforeEach(function() { ctrl.variable = { - current: {text: 'hej', value: 'hej'}, + current: {text: 'server-1', value: 'server-1'}, options: [ {text: 'server-1', value: 'server-1'}, {text: 'server-2', value: 'server-2'}, {text: 'server-3', value: 'server-3'}, ], - tags: ["key1", "key2", "key3"] + tags: ["key1", "key2", "key3"], + multi: true }; tagValuesMap.key1 = ['server-1', 'server-3']; tagValuesMap.key2 = ['server-2', 'server-3']; @@ -59,10 +61,30 @@ function () { expect(ctrl.options.length).to.be(3); }); + it("should init selected values array", function() { + expect(ctrl.selectedValues.length).to.be(1); + }); + + it("should set linkText", function() { + expect(ctrl.linkText).to.be('server-1'); + }); + + describe('after adititional value is selected', function() { + beforeEach(function() { + ctrl.selectValue(ctrl.options[2], {}); + ctrl.commitChanges(); + }); + + it('should update link text', function() { + expect(ctrl.linkText).to.be('server-1 + server-3'); + }); + }); + describe('When tag is selected', function() { beforeEach(function() { ctrl.selectTag(ctrl.tags[0]); rootScope.$digest(); + ctrl.commitChanges(); }); it("should select tag", function() { @@ -72,6 +94,11 @@ function () { it("should select values", function() { expect(ctrl.options[0].selected).to.be(true); expect(ctrl.options[2].selected).to.be(true); + expect(ctrl.linkText).to.be('server-1 + server-2'); + }); + + it("link text should not include tag values", function() { + expect(ctrl.linkText).to.not.contain('server-1'); }); describe('and then dropdown is opened and closed without changes', function() { @@ -99,7 +126,7 @@ function () { describe('and then value is unselected', function() { beforeEach(function() { - ctrl.optionSelected(ctrl.options[0]); + ctrl.selectValue(ctrl.options[0], {}); }); it("should deselect tag", function() { From b0451dc1b3483b875f5973f8e817574907139e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 19:32:50 +0200 Subject: [PATCH 257/398] More refinements of tag selection and state restoration after dashboard load --- public/app/directives/variableValueSelect.js | 48 ++++++++++++------- public/test/specs/selectDropdownCtrl-specs.js | 3 +- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 0c3ff0e3507..c89485130aa 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -27,7 +27,6 @@ function (angular, app, _) { }); vm.selectedValues = _.filter(vm.options, {selected: true}); - vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { vm.tags = _.map(vm.variable.tags, function(value) { @@ -40,21 +39,37 @@ function (angular, app, _) { }; vm.updateLinkText = function() { - // var currentValues = vm.variable.current.text; - // - // if (vm.variable.current.tags) { - // selectedOptions = _.filter(selectedOptions, function(test) { - // for (var i = 0; i < vm.variable.current.tags; i++) { - // var tag = vm.selectedTags[i]; - // if (_.indexOf(tag.values, test.text) !== -1) { - // return false; - // } - // } - // return true; - // }); - // } - // - vm.linkText = vm.variable.current.text; + var current = vm.variable.current; + var currentValues = current.value; + + if (_.isArray(currentValues) && current.tags.length) { + // filer out values that are in selected tags + currentValues = _.filter(currentValues, function(test) { + for (var i = 0; i < current.tags.length; i++) { + if (_.indexOf(current.tags[i].values, test) !== -1) { + return false; + } + } + return true; + }); + // convert values to text + var currentTexts = _.map(currentValues, function(value) { + for (var i = 0; i < vm.variable.options.length; i++) { + var option = vm.variable.options[i]; + if (option.value === value) { + return option.text; + } + } + return value; + }); + // join texts + vm.linkText = currentTexts.join(' + '); + if (vm.linkText.length > 0) { + vm.linkText += ' + '; + } + } else { + vm.linkText = vm.variable.current.text; + } }; vm.clearSelections = function() { @@ -202,6 +217,7 @@ function (angular, app, _) { }; vm.init = function() { + vm.selectedTags = vm.variable.current.tags || []; vm.updateLinkText(); }; diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index 603acc9a5fb..3aae3429a7c 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -94,11 +94,10 @@ function () { it("should select values", function() { expect(ctrl.options[0].selected).to.be(true); expect(ctrl.options[2].selected).to.be(true); - expect(ctrl.linkText).to.be('server-1 + server-2'); }); it("link text should not include tag values", function() { - expect(ctrl.linkText).to.not.contain('server-1'); + expect(ctrl.linkText).to.be(''); }); describe('and then dropdown is opened and closed without changes', function() { From 8934c83742a8a6cc0dc5126cc3bbcacfddb21efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 14:39:21 +0200 Subject: [PATCH 258/398] Small fixes for template variable groups (tags) --- public/app/directives/variableValueSelect.js | 2 ++ public/app/features/dashboard/partials/variableValueSelect.html | 2 +- public/app/features/templating/templateValuesSrv.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index c89485130aa..2ddb0dd932c 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -26,6 +26,8 @@ function (angular, app, _) { return option; }); + _.sortBy(vm.options, 'text'); + vm.selectedValues = _.filter(vm.options, {selected: true}); if (!vm.tags) { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 011522d6afe..25d220ab3a5 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -2,8 +2,8 @@ {{vm.linkText}} - {{tag.text}}     + {{tag.text}} diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 1cfa9bfc4ad..267a465cd89 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -153,6 +153,7 @@ function (angular, _, kbn) { }); }); } else { + delete variable.tags; return queryPromise; } }); From cb63344394c7048bb44153b16695588a7bf36d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 19:51:12 +0200 Subject: [PATCH 259/398] Added hover tooltip for tags --- public/app/directives/variableValueSelect.js | 2 +- .../features/dashboard/partials/variableValueSelect.html | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 2ddb0dd932c..94984977c7a 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -93,7 +93,7 @@ function (angular, app, _) { tagValuesPromise.then(function(values) { tag.values = values; - tag.valuesText = values.join(', '); + tag.valuesText = values.join(' + '); _.each(vm.options, function(option) { if (_.indexOf(tag.values, option.value) !== -1) { option.selected = tag.selected; diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 25d220ab3a5..2c291448573 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,9 +1,11 @@