mirror of https://github.com/grafana/grafana
commit
293d0c3093
@ -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); |
||||
}; |
||||
}); |
||||
|
||||
}); |
||||
@ -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; |
||||
}); |
||||
}; |
||||
|
||||
}); |
||||
|
||||
}); |
||||
@ -0,0 +1,26 @@ |
||||
<span class="template-variable" ng-show="!variable.hideLabel" style="padding-right: 5px"> |
||||
{{labelText}}: |
||||
</span> |
||||
|
||||
<div style="position: relative; display: inline-block"> |
||||
<a ng-click="show()" class="variable-value-link"> |
||||
{{linkText}} |
||||
<i class="fa fa-caret-down"></i> |
||||
</a> |
||||
|
||||
<div ng-if="selectorOpen" class="variable-value-dropdown"> |
||||
<div class="variable-search-wrapper"> |
||||
<span style="position: relative;"> |
||||
<input type="text" placeholder="Search values..." ng-keydown="keyDown($event)" give-focus="giveFocus" tabindex="1" ng-model="search.query" spellcheck='false' ng-change="queryChanged()" /> |
||||
</span> |
||||
</div> |
||||
|
||||
<div class="variable-options-container" ng-if="!query.tagcloud"> |
||||
<a class="variable-option pointer" bindonce ng-repeat="option in search.options" |
||||
ng-class="{'selected': option.selected, 'highlighted': $index === highlightIndex}" ng-click="optionSelected(option, $event)"> |
||||
<span >{{option.text}}</label> |
||||
<span class="fa fa-fw variable-option-icon"></span> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
@ -0,0 +1,180 @@ |
||||
define([ |
||||
'features/dashboard/dynamicDashboardSrv', |
||||
'features/dashboard/dashboardSrv' |
||||
], function() { |
||||
'use strict'; |
||||
|
||||
function dynamicDashScenario(desc, func) { |
||||
|
||||
describe(desc, function() { |
||||
var ctx = {}; |
||||
|
||||
ctx.setup = function (setupFunc) { |
||||
|
||||
beforeEach(module('grafana.services')); |
||||
|
||||
beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { |
||||
ctx.dynamicDashboardSrv = dynamicDashboardSrv; |
||||
ctx.dashboardSrv = dashboardSrv; |
||||
|
||||
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'}] |
||||
}); |
||||
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 repeat panel one time', function() { |
||||
expect(ctx.rows[0].panels.length).to.be(2); |
||||
}); |
||||
|
||||
it('should mark panel repeated', function() { |
||||
expect(ctx.rows[0].panels[0].repeat).to.be('apps'); |
||||
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); |
||||
}); |
||||
|
||||
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'); |
||||
}); |
||||
|
||||
describe('After a second iteration', function() { |
||||
var repeatedPanelAfterIteration1; |
||||
|
||||
beforeEach(function() { |
||||
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; |
||||
ctx.rows[0].panels[0].fill = 10; |
||||
ctx.dynamicDashboardSrv.update(ctx.dash); |
||||
}); |
||||
|
||||
it('should have reused same panel instances', function() { |
||||
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); |
||||
}); |
||||
}); |
||||
|
||||
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) { |
||||
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].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'); |
||||
}); |
||||
|
||||
describe('After a second iteration', function() { |
||||
var repeatedRowAfterFirstIteration; |
||||
|
||||
beforeEach(function() { |
||||
repeatedRowAfterFirstIteration = ctx.rows[1]; |
||||
ctx.rows[0].height = 500; |
||||
ctx.dynamicDashboardSrv.update(ctx.dash); |
||||
}); |
||||
|
||||
it('should still only have 2 rows', function() { |
||||
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); |
||||
}); |
||||
}); |
||||
|
||||
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); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,83 @@ |
||||
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); |
||||
$provide.value('$window', {}); |
||||
})); |
||||
|
||||
beforeEach(inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { |
||||
_unsavedChangesSrv = unsavedChangesSrv; |
||||
_dashboardSrv = dashboardSrv; |
||||
_location = $location; |
||||
_rootScope = $rootScope; |
||||
})); |
||||
|
||||
beforeEach(function() { |
||||
dash = _dashboardSrv.create({ |
||||
rows: [ |
||||
{ |
||||
panels: [{ test: "asd", legend: { } }] |
||||
} |
||||
] |
||||
}); |
||||
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); |
||||
}); |
||||
|
||||
it('Should ignore a lot of changes', function() { |
||||
dash.time = {from: '1h'}; |
||||
dash.refresh = true; |
||||
dash.schemaVersion = 10; |
||||
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); |
||||
}); |
||||
|
||||
}); |
||||
}); |
||||
Loading…
Reference in new issue