Commit 8bb9d92a by Torkel Ödegaard

grid: minor progress on panel repeats

parent 215d5986
......@@ -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,
......
......@@ -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() {
......
......@@ -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);
cleanUpRepeats() {
this.processRepeats(true);
}
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";
}
processRepeats(cleanUpOnly?: boolean) {
if (this.snapshot || this.templating.list.length === 0) {
return;
}
this.iteration = (this.iteration || new Date().getTime()) + 1;
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);
let panelsToRemove = [];
for (let panel of this.panels) {
if (panel.repeat) {
if (!cleanUpOnly) {
this.repeatPanel(panel);
}
});
return;
} 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);
......
......@@ -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));
}
......
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);
///<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 = {};
......
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;
......
......@@ -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}");
......
......@@ -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>
......
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);
}
......
......@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment