grid: minor progress on panel repeats

This commit is contained in:
Torkel Ödegaard 2017-10-12 21:37:27 +02:00
parent 215d59865e
commit 8bb9d92a73
12 changed files with 431 additions and 551 deletions

View File

@ -51,8 +51,10 @@ import {userGroupPicker} from './components/user_group_picker';
import {geminiScrollbar} from './components/scroll/scroll';
import {gfPageDirective} from './components/gf_page';
import {orgSwitcher} from './components/org_switcher';
import {profiler} from './profiler';
export {
profiler,
arrayJoin,
coreModule,
grafanaAppDirective,

View File

@ -20,7 +20,6 @@ export class DashboardCtrl implements PanelContainer {
private alertingSrv,
private dashboardSrv,
private unsavedChangesSrv,
private dynamicDashboardSrv,
private dashboardViewStateSrv,
private panelLoader) {
// temp hack due to way dashboards are loaded
@ -57,10 +56,9 @@ export class DashboardCtrl implements PanelContainer {
.catch(this.onInitFailed.bind(this, 'Templating init failed', false))
// continue
.finally(() => {
this.dashboard = dashboard;
this.dynamicDashboardSrv.init(dashboard);
this.dynamicDashboardSrv.process();
this.dashboard = dashboard;
this.dashboard.processRepeats();
this.unsavedChangesSrv.init(dashboard, this.$scope);
@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer {
}
templateVariableUpdated() {
this.dynamicDashboardSrv.process();
this.dashboard.processRepeats();
}
setWindowTitleAndTheme() {

View File

@ -2,7 +2,7 @@ import moment from 'moment';
import _ from 'lodash';
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
import {Emitter, contextSrv, appEvents} from 'app/core/core';
import {Emitter, contextSrv} from 'app/core/core';
import {DashboardRow} from './row/row_model';
import {PanelModel} from './panel_model';
import sortByKeys from 'app/core/utils/sort_by_keys';
@ -34,12 +34,19 @@ export class DashboardModel {
revision: number;
links: any;
gnetId: any;
meta: any;
events: any;
editMode: boolean;
folderId: number;
panels: PanelModel[];
// ------------------
// not persisted
// ------------------
// repeat process cycles
iteration: number;
meta: any;
events: Emitter;
static nonPersistedProperties: {[str: string]: boolean} = {
"events": true,
"meta": true,
@ -193,7 +200,12 @@ export class DashboardModel {
this.panels.unshift(new PanelModel(panel));
// make sure it's sorted by pos
this.sortPanelsByGridPos();
this.events.emit('panel-added', panel);
}
private sortPanelsByGridPos() {
this.panels.sort(function(panelA, panelB) {
if (panelA.gridPos.y === panelB.gridPos.y) {
return panelA.gridPos.x - panelB.gridPos.x;
@ -201,33 +213,86 @@ export class DashboardModel {
return panelA.gridPos.y - panelB.gridPos.y;
}
});
this.events.emit('panel-added', panel);
}
removePanel(panel, ask?) {
// confirm deletion
if (ask !== false) {
var text2, confirmText;
if (panel.alert) {
text2 = "Panel includes an alert rule, removing panel will also remove alert rule";
confirmText = "YES";
}
cleanUpRepeats() {
this.processRepeats(true);
}
appEvents.emit('confirm-modal', {
title: 'Remove Panel',
text: 'Are you sure you want to remove this panel?',
text2: text2,
icon: 'fa-trash',
confirmText: confirmText,
yesText: 'Remove',
onConfirm: () => {
this.removePanel(panel, false);
}
});
processRepeats(cleanUpOnly?: boolean) {
if (this.snapshot || this.templating.list.length === 0) {
return;
}
this.iteration = (this.iteration || new Date().getTime()) + 1;
let panelsToRemove = [];
for (let panel of this.panels) {
if (panel.repeat) {
if (!cleanUpOnly) {
this.repeatPanel(panel);
}
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
panelsToRemove.push(panel);
}
}
// remove panels
_.pull(this.panels, ...panelsToRemove);
this.sortPanelsByGridPos();
this.events.emit('repeats-processed');
}
getRepeatClone(sourcePanel, index) {
// if first clone return source
if (index === 0) {
return sourcePanel;
}
var clone = new PanelModel(sourcePanel.getSaveModel());
clone.id = this.getNextPanelId();
this.panels.push(clone);
clone.repeatIteration = this.iteration;
clone.repeatPanelId = sourcePanel.id;
clone.repeat = null;
return clone;
}
repeatPanel(panel: PanelModel) {
var variable = _.find(this.templating.list, {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});
}
for (let index = 0; index < selected.length; index++) {
var option = selected[index];
var copy = this.getRepeatClone(panel, index);
copy.scopedVars = copy.scopedVars || {};
copy.scopedVars[variable.name] = option;
// souce panel uses original possition
if (index === 0) {
continue;
}
if (panel.repeatDirection === 'Y') {
copy.gridPos.y = panel.gridPos.y + (panel.gridPos.h*index);
} else {
copy.gridPos.x = panel.gridPos.x + (panel.gridPos.w*index);
}
}
}
removePanel(panel: PanelModel) {
var index = _.indexOf(this.panels, panel);
this.panels.splice(index, 1);
this.events.emit('panel-removed', panel);

View File

@ -69,6 +69,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
this.dashboard = this.panelContainer.getDashboard();
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
}

View File

@ -1,192 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import {DashboardRow} from './row/row_model';
export class DynamicDashboardSrv {
iteration: number;
dashboard: any;
variables: any;
init(dashboard) {
this.dashboard = dashboard;
this.variables = dashboard.templating.list;
}
process(options?) {
if (this.dashboard.snapshot || this.variables.length === 0) {
return;
}
this.iteration = (this.iteration || new Date().getTime()) + 1;
options = options || {};
var cleanUpOnly = options.cleanUpOnly;
var i, j, row, panel;
if (this.dashboard.rows) {
// cleanup scopedVars
for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i];
delete row.scopedVars;
for (j = 0; j < row.panels.length; j++) {
delete row.panels[j].scopedVars;
}
}
for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i];
// handle row repeats
if (row.repeat) {
if (!cleanUpOnly) {
this.repeatRow(row, i);
}
} else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
// clean up old left overs
this.dashboard.removeRow(row, true);
i = i - 1;
continue;
}
// repeat panels
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.repeat) {
if (!cleanUpOnly) {
this.repeatPanel(panel, row);
}
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
// clean up old left overs
row.panels = _.without(row.panels, panel);
j = j - 1;
}
}
row.panelSpanChanged();
}
}
}
// returns a new row clone or reuses a clone from previous iteration
getRowClone(sourceRow, repeatIndex, sourceRowIndex) {
if (repeatIndex === 0) {
return sourceRow;
}
var i, panel, row, copy;
var sourceRowId = sourceRowIndex + 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;
copy.copyPropertiesFromRowSource(sourceRow);
break;
}
}
if (!copy) {
var modelCopy = angular.copy(sourceRow.getSaveModel());
copy = new DashboardRow(modelCopy);
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, 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 row clone or reuses a clone from previous iteration
repeatRow(row, rowIndex) {
var variable = _.find(this.variables, {name: row.repeat});
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, (option, index) => {
copy = this.getRowClone(row, index, rowIndex);
copy.scopedVars = {};
copy.scopedVars[variable.name] = option;
for (i = 0; i < copy.panels.length; i++) {
panel = copy.panels[i];
panel.scopedVars = {};
panel.scopedVars[variable.name] = option;
}
});
}
getPanelClone(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.copy(sourcePanel, clone);
// restore id
clone.id = tmpId;
clone.repeatIteration = this.iteration;
clone.repeatPanelId = sourcePanel.id;
clone.repeat = null;
return clone;
}
repeatPanel(panel, row) {
var variable = _.find(this.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, (option, index) => {
var copy = this.getPanelClone(panel, row, index);
copy.span = Math.max(12 / selected.length, panel.minSpan || 4);
copy.scopedVars = copy.scopedVars || {};
copy.scopedVars[variable.name] = option;
});
}
}
coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv);

View File

@ -1,29 +1,24 @@
///<reference path="../../../headers/common.d.ts" />
import config from 'app/core/config';
import _ from 'lodash';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
import {DashboardModel} from '../dashboard_model';
export class DashboardExporter {
constructor(private datasourceSrv) {
}
makeExportable(dashboard) {
var dynSrv = new DynamicDashboardSrv();
makeExportable(dashboard: DashboardModel) {
// clean up repeated rows and panels,
// this is done on the live real dashboard instance, not on a clone
// so we need to undo this
// this is pretty hacky and needs to be changed
dynSrv.init(dashboard);
dynSrv.process({cleanUpOnly: true});
dashboard.cleanUpRepeats();
var saveModel = dashboard.getSaveModelClone();
saveModel.id = null;
// undo repeat cleanup
dynSrv.process();
dashboard.processRepeats();
var inputs = [];
var requires = {};

View File

@ -1,4 +1,5 @@
import {Emitter} from 'app/core/core';
import _ from 'lodash';
export interface GridPos {
x: number;
@ -21,6 +22,9 @@ export class PanelModel {
alert?: any;
scopedVars?: any;
repeat?: any;
repeatIteration?: any;
repeatPanelId?: any;
repeatDirection?: any;
// non persisted
fullscreen: boolean;
@ -34,6 +38,10 @@ export class PanelModel {
for (var property in model) {
this[property] = model[property];
}
if (!this.gridPos) {
this.gridPos = {x: 0, y: 0, h: 3, w: 6};
}
}
getSaveModel() {
@ -43,7 +51,7 @@ export class PanelModel {
continue;
}
model[property] = this[property];
model[property] = _.cloneDeep(this[property]);
}
return model;

View File

@ -1,287 +1,287 @@
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import '../dashboard_srv';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
function dynamicDashScenario(desc, func) {
describe.skip(desc, function() {
var ctx: any = {};
ctx.setup = function (setupFunc) {
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module(function($provide) {
$provide.value('contextSrv', {
user: { timezone: 'utc'}
});
}));
beforeEach(angularMocks.inject(function(dashboardSrv) {
ctx.dashboardSrv = dashboardSrv;
var model = {
rows: [],
templating: { list: [] }
};
setupFunc(model);
ctx.dash = ctx.dashboardSrv.create(model);
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
ctx.dynamicDashboardSrv.init(ctx.dash);
ctx.dynamicDashboardSrv.process();
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, se3',
value: ['se1', 'se2', 'se3']
},
options: [
{text: 'se1', value: 'se1', selected: true},
{text: 'se2', value: 'se2', selected: true},
{text: 'se3', value: 'se3', selected: true},
{text: 'se4', value: 'se4', selected: false}
]
});
});
it('should repeat panel one time', function() {
expect(ctx.rows[0].panels.length).to.be(3);
});
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');
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
});
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.process();
});
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(3);
});
});
describe('After a second iteration with different variable', function() {
beforeEach(function() {
ctx.dash.templating.list.push({
name: 'server',
current: { text: 'se1, se2, se3', value: ['se1']},
options: [{text: 'se1', value: 'se1', selected: true}]
});
ctx.rows[0].panels[0].repeat = "server";
ctx.dynamicDashboardSrv.process();
});
it('should remove scopedVars value for last variable', function() {
expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined);
});
it('should have new variable value in scopedVars', function() {
expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1");
});
});
describe('After a second iteration and selected values reduced', function() {
beforeEach(function() {
ctx.dash.templating.list[0].options[1].selected = false;
ctx.dynamicDashboardSrv.process();
});
it('should clean up repeated panel', function() {
expect(ctx.rows[0].panels.length).to.be(2);
});
});
describe('After a second iteration and panel repeat is turned off', function() {
beforeEach(function() {
ctx.rows[0].panels[0].repeat = null;
ctx.dynamicDashboardSrv.process();
});
it('should clean up repeated panel', function() {
expect(ctx.rows[0].panels.length).to.be(1);
});
it('should remove scoped vars from reused panel', function() {
expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined);
});
});
});
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
ctx.setup(function(dash) {
dash.rows.push({
repeat: 'servers',
panels: [{id: 2}]
});
dash.rows.push({panels: []});
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(3);
});
it('should keep panel ids on first row', function() {
expect(ctx.rows[0].panels[0].id).to.be(2);
});
it('should keep first row as repeat', 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 add scopedVars to rows', function() {
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
});
it('should generate a repeartRowId based on repeat row index', function() {
expect(ctx.rows[1].repeatRowId).to.be(1);
expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration);
});
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.process();
});
it('should still only have 2 rows', function() {
expect(ctx.rows.length).to.be(3);
});
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.process();
});
it('should remove repeated second row', function() {
expect(ctx.rows.length).to.be(2);
});
});
});
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');
});
});
// import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
//
// import '../dashboard_srv';
// import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
//
// function dynamicDashScenario(desc, func) {
//
// describe.skip(desc, function() {
// var ctx: any = {};
//
// ctx.setup = function (setupFunc) {
//
// beforeEach(angularMocks.module('grafana.core'));
// beforeEach(angularMocks.module('grafana.services'));
// beforeEach(angularMocks.module(function($provide) {
// $provide.value('contextSrv', {
// user: { timezone: 'utc'}
// });
// }));
//
// beforeEach(angularMocks.inject(function(dashboardSrv) {
// ctx.dashboardSrv = dashboardSrv;
//
// var model = {
// rows: [],
// templating: { list: [] }
// };
//
// setupFunc(model);
// ctx.dash = ctx.dashboardSrv.create(model);
// ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
// ctx.dynamicDashboardSrv.init(ctx.dash);
// ctx.dynamicDashboardSrv.process();
// 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, se3',
// value: ['se1', 'se2', 'se3']
// },
// options: [
// {text: 'se1', value: 'se1', selected: true},
// {text: 'se2', value: 'se2', selected: true},
// {text: 'se3', value: 'se3', selected: true},
// {text: 'se4', value: 'se4', selected: false}
// ]
// });
// });
//
// it('should repeat panel one time', function() {
// expect(ctx.rows[0].panels.length).to.be(3);
// });
//
// 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');
// expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
// });
//
// 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.process();
// });
//
// 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(3);
// });
// });
//
// describe('After a second iteration with different variable', function() {
// beforeEach(function() {
// ctx.dash.templating.list.push({
// name: 'server',
// current: { text: 'se1, se2, se3', value: ['se1']},
// options: [{text: 'se1', value: 'se1', selected: true}]
// });
// ctx.rows[0].panels[0].repeat = "server";
// ctx.dynamicDashboardSrv.process();
// });
//
// it('should remove scopedVars value for last variable', function() {
// expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined);
// });
//
// it('should have new variable value in scopedVars', function() {
// expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1");
// });
// });
//
// describe('After a second iteration and selected values reduced', function() {
// beforeEach(function() {
// ctx.dash.templating.list[0].options[1].selected = false;
// ctx.dynamicDashboardSrv.process();
// });
//
// it('should clean up repeated panel', function() {
// expect(ctx.rows[0].panels.length).to.be(2);
// });
// });
//
// describe('After a second iteration and panel repeat is turned off', function() {
// beforeEach(function() {
// ctx.rows[0].panels[0].repeat = null;
// ctx.dynamicDashboardSrv.process();
// });
//
// it('should clean up repeated panel', function() {
// expect(ctx.rows[0].panels.length).to.be(1);
// });
//
// it('should remove scoped vars from reused panel', function() {
// expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined);
// });
// });
//
// });
//
// dynamicDashScenario('given dashboard with row repeat', function(ctx) {
// ctx.setup(function(dash) {
// dash.rows.push({
// repeat: 'servers',
// panels: [{id: 2}]
// });
// dash.rows.push({panels: []});
// 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(3);
// });
//
// it('should keep panel ids on first row', function() {
// expect(ctx.rows[0].panels[0].id).to.be(2);
// });
//
// it('should keep first row as repeat', 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 add scopedVars to rows', function() {
// expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
// expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
// });
//
// it('should generate a repeartRowId based on repeat row index', function() {
// expect(ctx.rows[1].repeatRowId).to.be(1);
// expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration);
// });
//
// 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.process();
// });
//
// it('should still only have 2 rows', function() {
// expect(ctx.rows.length).to.be(3);
// });
//
// 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.process();
// });
//
// it('should remove repeated second row', function() {
// expect(ctx.rows.length).to.be(2);
// });
// });
// });
//
// 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');
// });
//
// });

View File

@ -10,7 +10,6 @@ describe('given dashboard with repeated panels', function() {
beforeEach(done => {
dash = {
rows: [],
templating: { list: [] },
annotations: { list: [] },
};
@ -47,26 +46,6 @@ describe('given dashboard with repeated panels', function() {
datasource: 'gfdb',
});
dash.rows.push({
repeat: 'test',
panels: [
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
{id: 3, repeat: null, repeatPanelId: 2},
{
id: 4,
datasource: '-- Mixed --',
targets: [{datasource: 'other'}],
},
{id: 5, datasource: '$ds'},
]
});
dash.rows.push({
repeat: null,
repeatRowId: 1,
panels: [],
});
dash.panels = [
{id: 6, datasource: 'gfdb', type: 'graph'},
{id: 7},
@ -78,6 +57,9 @@ describe('given dashboard with repeated panels', function() {
{id: 9, datasource: '$ds'},
];
dash.panels.push({id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'});
dash.panels.push({id: 3, repeat: null, repeatPanelId: 2});
var datasourceSrvStub = {get: sinon.stub()};
datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({
name: 'gfdb',
@ -110,14 +92,6 @@ describe('given dashboard with repeated panels', function() {
});
});
it.skip('exported dashboard should not contain repeated panels', function() {
expect(exported.rows[0].panels.length).to.be(3);
});
it.skip('exported dashboard should not contain repeated rows', function() {
expect(exported.rows.length).to.be(1);
});
it('should replace datasource refs', function() {
var panel = exported.panels[0];
expect(panel.datasource).to.be("${DS_GFDB}");

View File

@ -22,9 +22,9 @@ const template = `
</div>
<div class="confirm-modal-buttons">
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="ctrl.discard()">Discard</button>
<button type="button" class="btn btn-success" ng-click="ctrl.save()">Save</button>
<button type="button" class="btn btn-danger" ng-click="ctrl.discard()">Discard</button>
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
import config from 'app/core/config';
import _ from 'lodash';
import $ from 'jquery';
import {profiler} from 'app/core/profiler';
import {appEvents, profiler} from 'app/core/core';
import Remarkable from 'remarkable';
import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/dashboard_model';
@ -188,7 +188,30 @@ export class PanelCtrl {
});
}
removePanel() {
removePanel(ask: boolean) {
// confirm deletion
if (ask !== false) {
var text2, confirmText;
if (this.panel.alert) {
text2 = "Panel includes an alert rule, removing panel will also remove alert rule";
confirmText = "YES";
}
appEvents.emit('confirm-modal', {
title: 'Remove Panel',
text: 'Are you sure you want to remove this panel?',
text2: text2,
icon: 'fa-trash',
confirmText: confirmText,
yesText: 'Remove',
onConfirm: () => {
this.removePanel(false);
}
});
return;
}
this.dashboard.removePanel(this.panel);
}

View File

@ -24,6 +24,12 @@
<option value=""></option>
</select>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Direction</span>
<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f for f in ['X', 'Y']">
<option value=""></option>
</select>
</div>
</div>
<panel-links-editor panel="ctrl.panel"></panel-links-editor>