diff --git a/public/app/controllers/search.js b/public/app/controllers/search.js index ff171c953a2..396b3f6a3d8 100644 --- a/public/app/controllers/search.js +++ b/public/app/controllers/search.js @@ -140,25 +140,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() { function djb2(str) { diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 6190ef89099..3ef0b669d6c 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -16,4 +16,5 @@ define([ './grafanaVersionCheck', './dropdown.typeahead', './topnav', + './giveFocus', ], function () {}); diff --git a/public/app/directives/giveFocus.js b/public/app/directives/giveFocus.js new file mode 100644 index 00000000000..ef395d27fbd --- /dev/null +++ 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/directives/templateParamSelector.js b/public/app/directives/templateParamSelector.js index d9ad33512b5..fa0e56acebc 100644 --- a/public/app/directives/templateParamSelector.js +++ b/public/app/directives/templateParamSelector.js @@ -4,84 +4,156 @@ define([ 'lodash', 'jquery', ], -function (angular, app, _, $) { +function (angular, app, _) { 'use strict'; angular .module('grafana.directives') - .directive('templateParamSelector', function($compile) { - var inputTemplate = ''; - - var buttonTemplate = '{{variable.current.text}} '; - + .directive('variableValueSelect', function($compile, $window, $timeout) { return { - link: function($scope, elem) { - var $input = $(inputTemplate); - var $button = $(buttonTemplate); - var variable = $scope.variable; + 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; - $input.appendTo(elem); - $button.appendTo(elem); - - function updateVariableValue(value) { - $scope.$apply(function() { - var selected = _.findWhere(variable.options, { text: value }); - if (!selected) { - selected = { text: value, value: value }; - } - $scope.setVariableValue($scope.variable, selected); - }); - } - - $input.attr('data-provide', 'typeahead'); - $input.typeahead({ - minLength: 0, - items: 1000, - updater: function(value) { - $input.val(value); - $input.trigger('blur'); - return value; + scope.show = function() { + if (scope.selectorOpen) { + return; } - }); - var typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - var options = _.map(variable.options, function(option) { return option.text; }); - this.query = this.$element.val() || ''; - return this.process(options); + scope.selectorOpen = true; + scope.giveFocus = 1; + scope.oldCurrentText = variable.current.text; + scope.highlightIndex = -1; + + var currentValues = variable.current.value; + + if (_.isString(currentValues)) { + currentValues = [currentValues]; + } + + scope.options = _.map(variable.options, function(option) { + if (_.indexOf(currentValues, option.value) >= 0) { + option.selected = true; + } + return option; + }); + + scope.search = {query: '', options: scope.options}; + + $timeout(function() { + bodyEl.on('click', scope.bodyOnClick); + }, 0, false); }; - $button.click(function() { - $input.css('width', ($button.width() + 16) + 'px'); + scope.queryChanged = function() { + scope.highlightIndex = -1; + scope.search.options = _.filter(scope.options, function(option) { + return option.text.toLowerCase().indexOf(scope.search.query.toLowerCase()) !== -1; + }); + }; - $button.hide(); - $input.show(); - $input.focus(); + 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], {}); + } + }; - var typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); + scope.moveHighlight = function(direction) { + scope.highlightIndex = (scope.highlightIndex + direction) % scope.search.options.length; + }; + + scope.optionSelected = function(option, event) { + option.selected = !option.selected; + + var hideAfter = true; + var setAllExceptCurrentTo = function(newValue) { + _.each(scope.options, function(other) { + 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}); - $input.blur(function() { - if ($input.val() !== '') { updateVariableValue($input.val()); } - $input.hide(); - $button.show(); - $button.focus(); - }); + if (selected.length === 0) { + option.selected = true; + selected = [option]; + } - $scope.$on('$destroy', function() { - $button.unbind(); - typeahead.destroy(); - }); + if (selected.length > 1 && selected.length !== scope.options.length) { + if (selected[0].text === 'All') { + selected[0].selected = false; + selected = selected.slice(1, selected.length); + } + } - $compile(elem.contents())($scope); - } + 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.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.updateLinkText = function() { + scope.labelText = variable.label || '$' + variable.name; + scope.linkText = variable.current.text; + }; + + scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() { + scope.updateLinkText(); + }); + }, }; }); + }); diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index a279e9f3078..aeabe6b354e 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -16,5 +16,6 @@ define([ './unsavedChangesSrv', './directives/dashSearchView', './graphiteImportCtrl', + './dynamicDashboardSrv', './importCtrl', ], function () {}); diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ff915a7dbf2..f81b808a0e1 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -15,7 +15,9 @@ function (angular, $, config) { dashboardKeybindings, timeSrv, templateValuesSrv, + dynamicDashboardSrv, dashboardSrv, + unsavedChangesSrv, dashboardViewStateSrv, contextSrv, $timeout) { @@ -46,6 +48,9 @@ function (angular, $, config) { // template values service needs to initialize completely before // 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; $scope.dashboardViewState = dashboardViewStateSrv.create($scope); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 9324f18e33b..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; @@ -180,6 +176,7 @@ function (angular, $, kbn, _, moment) { var currentRow = this.rows[rowIndex]; currentRow.panels.push(newPanel); + return newPanel; }; p.formatDate = function(date, format) { diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js new file mode 100644 index 00000000000..cfa824e3402 --- /dev/null +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -0,0 +1,172 @@ +define([ + 'angular', + 'lodash', +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('dynamicDashboardSrv', function() { + var self = this; + + this.init = function(dashboard) { + this.iteration = new Date().getTime(); + this.process(dashboard); + }; + + this.update = function(dashboard) { + this.iteration = this.iteration + 1; + this.process(dashboard); + }; + + this.process = function(dashboard) { + if (dashboard.templating.list.length === 0) { return; } + this.dashboard = dashboard; + + 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) { + this.repeatPanel(panel, row); + } + // clean up old left overs + else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + row.panels = _.without(row.panels, panel); + 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; + } + } + }; + + // returns a new row clone or reuses a clone from previous iteration + this.getRowClone = function(sourceRow, index) { + if (index === 0) { + return sourceRow; + } + + 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) { + copy = row; + break; + } + } + + 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(); + } + } + + copy.repeat = null; + copy.repeatRowId = sourceRowId; + copy.repeatIteration = this.iteration; + 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('$', '')}); + 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) { + copy = self.getRowClone(row, index); + + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.scopedVars = panel.scopedVars || {}; + panel.scopedVars[variable.name] = option; + } + }); + }; + + this.getPanelClone = function(sourcePanel, row, index) { + // if first clone return source + if (index === 0) { + return sourcePanel; + } + + var i, tmpId, panel, clone; + + // first try finding an existing clone to use + for (i = 0; i < row.panels.length; i++) { + panel = row.panels[i]; + if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { + clone = panel; + break; + } + } + + 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; + return clone; + }; + + 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') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, function(option, index) { + var copy = self.getPanelClone(panel, row, index); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + }); + }; + + }); + +}); 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 @@
  • -
  • +