Commit 8bb9d92a by Torkel Ödegaard

grid: minor progress on panel repeats

parent 215d5986
...@@ -51,8 +51,10 @@ import {userGroupPicker} from './components/user_group_picker'; ...@@ -51,8 +51,10 @@ import {userGroupPicker} from './components/user_group_picker';
import {geminiScrollbar} from './components/scroll/scroll'; import {geminiScrollbar} from './components/scroll/scroll';
import {gfPageDirective} from './components/gf_page'; import {gfPageDirective} from './components/gf_page';
import {orgSwitcher} from './components/org_switcher'; import {orgSwitcher} from './components/org_switcher';
import {profiler} from './profiler';
export { export {
profiler,
arrayJoin, arrayJoin,
coreModule, coreModule,
grafanaAppDirective, grafanaAppDirective,
......
...@@ -20,7 +20,6 @@ export class DashboardCtrl implements PanelContainer { ...@@ -20,7 +20,6 @@ export class DashboardCtrl implements PanelContainer {
private alertingSrv, private alertingSrv,
private dashboardSrv, private dashboardSrv,
private unsavedChangesSrv, private unsavedChangesSrv,
private dynamicDashboardSrv,
private dashboardViewStateSrv, private dashboardViewStateSrv,
private panelLoader) { private panelLoader) {
// temp hack due to way dashboards are loaded // temp hack due to way dashboards are loaded
...@@ -57,10 +56,9 @@ export class DashboardCtrl implements PanelContainer { ...@@ -57,10 +56,9 @@ export class DashboardCtrl implements PanelContainer {
.catch(this.onInitFailed.bind(this, 'Templating init failed', false)) .catch(this.onInitFailed.bind(this, 'Templating init failed', false))
// continue // continue
.finally(() => { .finally(() => {
this.dashboard = dashboard;
this.dynamicDashboardSrv.init(dashboard); this.dashboard = dashboard;
this.dynamicDashboardSrv.process(); this.dashboard.processRepeats();
this.unsavedChangesSrv.init(dashboard, this.$scope); this.unsavedChangesSrv.init(dashboard, this.$scope);
...@@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer { ...@@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer {
} }
templateVariableUpdated() { templateVariableUpdated() {
this.dynamicDashboardSrv.process(); this.dashboard.processRepeats();
} }
setWindowTitleAndTheme() { setWindowTitleAndTheme() {
......
...@@ -2,7 +2,7 @@ import moment from 'moment'; ...@@ -2,7 +2,7 @@ import moment from 'moment';
import _ from 'lodash'; import _ from 'lodash';
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; 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 {DashboardRow} from './row/row_model';
import {PanelModel} from './panel_model'; import {PanelModel} from './panel_model';
import sortByKeys from 'app/core/utils/sort_by_keys'; import sortByKeys from 'app/core/utils/sort_by_keys';
...@@ -34,12 +34,19 @@ export class DashboardModel { ...@@ -34,12 +34,19 @@ export class DashboardModel {
revision: number; revision: number;
links: any; links: any;
gnetId: any; gnetId: any;
meta: any;
events: any;
editMode: boolean; editMode: boolean;
folderId: number; folderId: number;
panels: PanelModel[]; panels: PanelModel[];
// ------------------
// not persisted
// ------------------
// repeat process cycles
iteration: number;
meta: any;
events: Emitter;
static nonPersistedProperties: {[str: string]: boolean} = { static nonPersistedProperties: {[str: string]: boolean} = {
"events": true, "events": true,
"meta": true, "meta": true,
...@@ -193,7 +200,12 @@ export class DashboardModel { ...@@ -193,7 +200,12 @@ export class DashboardModel {
this.panels.unshift(new PanelModel(panel)); 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) { this.panels.sort(function(panelA, panelB) {
if (panelA.gridPos.y === panelB.gridPos.y) { if (panelA.gridPos.y === panelB.gridPos.y) {
return panelA.gridPos.x - panelB.gridPos.x; return panelA.gridPos.x - panelB.gridPos.x;
...@@ -201,33 +213,86 @@ export class DashboardModel { ...@@ -201,33 +213,86 @@ export class DashboardModel {
return panelA.gridPos.y - panelB.gridPos.y; return panelA.gridPos.y - panelB.gridPos.y;
} }
}); });
}
this.events.emit('panel-added', panel); cleanUpRepeats() {
this.processRepeats(true);
} }
removePanel(panel, ask?) { processRepeats(cleanUpOnly?: boolean) {
// confirm deletion if (this.snapshot || this.templating.list.length === 0) {
if (ask !== false) { return;
var text2, confirmText; }
if (panel.alert) {
text2 = "Panel includes an alert rule, removing panel will also remove alert rule"; this.iteration = (this.iteration || new Date().getTime()) + 1;
confirmText = "YES";
}
appEvents.emit('confirm-modal', { let panelsToRemove = [];
title: 'Remove Panel',
text: 'Are you sure you want to remove this panel?', for (let panel of this.panels) {
text2: text2, if (panel.repeat) {
icon: 'fa-trash', if (!cleanUpOnly) {
confirmText: confirmText, this.repeatPanel(panel);
yesText: 'Remove',
onConfirm: () => {
this.removePanel(panel, false);
} }
}); } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
return; 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); var index = _.indexOf(this.panels, panel);
this.panels.splice(index, 1); this.panels.splice(index, 1);
this.events.emit('panel-removed', panel); this.events.emit('panel-removed', panel);
......
...@@ -69,6 +69,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> { ...@@ -69,6 +69,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
this.dashboard = this.panelContainer.getDashboard(); this.dashboard = this.panelContainer.getDashboard();
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
this.dashboard.on('panel-removed', 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)); 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 config from 'app/core/config';
import _ from 'lodash'; import _ from 'lodash';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; import {DashboardModel} from '../dashboard_model';
export class DashboardExporter { export class DashboardExporter {
constructor(private datasourceSrv) { constructor(private datasourceSrv) {
} }
makeExportable(dashboard) { makeExportable(dashboard: DashboardModel) {
var dynSrv = new DynamicDashboardSrv();
// clean up repeated rows and panels, // clean up repeated rows and panels,
// this is done on the live real dashboard instance, not on a clone // this is done on the live real dashboard instance, not on a clone
// so we need to undo this // so we need to undo this
// this is pretty hacky and needs to be changed // this is pretty hacky and needs to be changed
dynSrv.init(dashboard); dashboard.cleanUpRepeats();
dynSrv.process({cleanUpOnly: true});
var saveModel = dashboard.getSaveModelClone(); var saveModel = dashboard.getSaveModelClone();
saveModel.id = null; saveModel.id = null;
// undo repeat cleanup // undo repeat cleanup
dynSrv.process(); dashboard.processRepeats();
var inputs = []; var inputs = [];
var requires = {}; var requires = {};
......
import {Emitter} from 'app/core/core'; import {Emitter} from 'app/core/core';
import _ from 'lodash';
export interface GridPos { export interface GridPos {
x: number; x: number;
...@@ -21,6 +22,9 @@ export class PanelModel { ...@@ -21,6 +22,9 @@ export class PanelModel {
alert?: any; alert?: any;
scopedVars?: any; scopedVars?: any;
repeat?: any; repeat?: any;
repeatIteration?: any;
repeatPanelId?: any;
repeatDirection?: any;
// non persisted // non persisted
fullscreen: boolean; fullscreen: boolean;
...@@ -34,6 +38,10 @@ export class PanelModel { ...@@ -34,6 +38,10 @@ export class PanelModel {
for (var property in model) { for (var property in model) {
this[property] = model[property]; this[property] = model[property];
} }
if (!this.gridPos) {
this.gridPos = {x: 0, y: 0, h: 3, w: 6};
}
} }
getSaveModel() { getSaveModel() {
...@@ -43,7 +51,7 @@ export class PanelModel { ...@@ -43,7 +51,7 @@ export class PanelModel {
continue; continue;
} }
model[property] = this[property]; model[property] = _.cloneDeep(this[property]);
} }
return model; return model;
......
...@@ -10,7 +10,6 @@ describe('given dashboard with repeated panels', function() { ...@@ -10,7 +10,6 @@ describe('given dashboard with repeated panels', function() {
beforeEach(done => { beforeEach(done => {
dash = { dash = {
rows: [],
templating: { list: [] }, templating: { list: [] },
annotations: { list: [] }, annotations: { list: [] },
}; };
...@@ -47,26 +46,6 @@ describe('given dashboard with repeated panels', function() { ...@@ -47,26 +46,6 @@ describe('given dashboard with repeated panels', function() {
datasource: 'gfdb', 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 = [ dash.panels = [
{id: 6, datasource: 'gfdb', type: 'graph'}, {id: 6, datasource: 'gfdb', type: 'graph'},
{id: 7}, {id: 7},
...@@ -78,6 +57,9 @@ describe('given dashboard with repeated panels', function() { ...@@ -78,6 +57,9 @@ describe('given dashboard with repeated panels', function() {
{id: 9, datasource: '$ds'}, {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()}; var datasourceSrvStub = {get: sinon.stub()};
datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({ datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({
name: 'gfdb', name: 'gfdb',
...@@ -110,14 +92,6 @@ describe('given dashboard with repeated panels', function() { ...@@ -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() { it('should replace datasource refs', function() {
var panel = exported.panels[0]; var panel = exported.panels[0];
expect(panel.datasource).to.be("${DS_GFDB}"); expect(panel.datasource).to.be("${DS_GFDB}");
......
...@@ -22,9 +22,9 @@ const template = ` ...@@ -22,9 +22,9 @@ const template = `
</div> </div>
<div class="confirm-modal-buttons"> <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-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> </div>
</div> </div>
......
import config from 'app/core/config'; import config from 'app/core/config';
import _ from 'lodash'; import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import {profiler} from 'app/core/profiler'; import {appEvents, profiler} from 'app/core/core';
import Remarkable from 'remarkable'; import Remarkable from 'remarkable';
import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/dashboard_model'; import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/dashboard_model';
...@@ -188,7 +188,30 @@ export class PanelCtrl { ...@@ -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); this.dashboard.removePanel(this.panel);
} }
......
...@@ -24,6 +24,12 @@ ...@@ -24,6 +24,12 @@
<option value=""></option> <option value=""></option>
</select> </select>
</div> </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> </div>
<panel-links-editor panel="ctrl.panel"></panel-links-editor> <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