Procházet zdrojové kódy

grid: minor progress on panel repeats

Torkel Ödegaard před 8 roky
rodič
revize
8bb9d92a73

+ 2 - 0
public/app/core/core.ts

@@ -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,

+ 3 - 5
public/app/features/dashboard/dashboard_ctrl.ts

@@ -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 {
       .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.dynamicDashboardSrv.process();
+        this.dashboard = dashboard;
+        this.dashboard.processRepeats();
 
 
         this.unsavedChangesSrv.init(dashboard, this.$scope);
         this.unsavedChangesSrv.init(dashboard, this.$scope);
 
 
@@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer {
     }
     }
 
 
     templateVariableUpdated() {
     templateVariableUpdated() {
-      this.dynamicDashboardSrv.process();
+      this.dashboard.processRepeats();
     }
     }
 
 
     setWindowTitleAndTheme() {
     setWindowTitleAndTheme() {

+ 89 - 24
public/app/features/dashboard/dashboard_model.ts

@@ -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 {
   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 {
 
 
     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 {
         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?) {
-    // 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);
     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);

+ 1 - 0
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -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));
   }
   }
 
 

+ 0 - 192
public/app/features/dashboard/dynamic_dashboard_srv.ts

@@ -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);
-

+ 4 - 9
public/app/features/dashboard/export/exporter.ts

@@ -1,29 +1,24 @@
-///<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) {
-    var dynSrv = new DynamicDashboardSrv();
-
+  makeExportable(dashboard: DashboardModel) {
     // 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);
-    dynSrv.process({cleanUpOnly: true});
+    dashboard.cleanUpRepeats();
 
 
     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 = {};

+ 9 - 1
public/app/features/dashboard/panel_model.ts

@@ -1,4 +1,5 @@
 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 {
   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 {
     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 {
         continue;
         continue;
       }
       }
 
 
-      model[property] = this[property];
+      model[property] = _.cloneDeep(this[property]);
     }
     }
 
 
     return model;
     return model;

+ 286 - 286
public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts

@@ -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');
+//   });
+//
+// });
 
 

+ 3 - 29
public/app/features/dashboard/specs/exporter_specs.ts

@@ -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() {
       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() {
       {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() {
     });
     });
   });
   });
 
 
-  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}");

+ 2 - 2
public/app/features/dashboard/unsaved_changes_modal.ts

@@ -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>

+ 25 - 2
public/app/features/panel/panel_ctrl.ts

@@ -1,7 +1,7 @@
 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 {
     });
     });
   }
   }
 
 
-  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);
   }
   }
 
 

+ 6 - 0
public/app/partials/panelgeneral.html

@@ -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>