Browse Source

Merge branch 'feat-10008' of https://github.com/alexanderzobnin/grafana into alexanderzobnin-feat-10008

Torkel Ödegaard 8 years ago
parent
commit
df693134bd

+ 135 - 27
public/app/features/dashboard/dashboard_model.ts

@@ -181,6 +181,14 @@ export class DashboardModel {
       if (panel.id > max) {
       if (panel.id > max) {
         max = panel.id;
         max = panel.id;
       }
       }
+
+      if (panel.collapsed) {
+        for (let rowPanel of panel.panels) {
+          if (rowPanel.id > max) {
+            max = rowPanel.id;
+          }
+        }
+      }
     }
     }
 
 
     return max + 1;
     return max + 1;
@@ -274,21 +282,11 @@ export class DashboardModel {
       return sourcePanel;
       return sourcePanel;
     }
     }
 
 
-    var clone = new PanelModel(sourcePanel.getSaveModel());
+    let clone = new PanelModel(sourcePanel.getSaveModel());
     clone.id = this.getNextPanelId();
     clone.id = this.getNextPanelId();
 
 
-    if (sourcePanel.type === 'row') {
-      // for row clones we need to figure out panels under row to clone and where to insert clone
-      let rowPanels = this.getRowPanels(sourcePanelIndex);
-      clone.panels = _.map(rowPanels, panel => panel.getSaveModel());
-
-      // insert after preceding row's panels
-      let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex);
-      this.panels.splice(insertPos, 0, clone);
-    } else {
-      // insert after source panel + value index
-      this.panels.splice(sourcePanelIndex+valueIndex, 0, clone);
-    }
+    // insert after source panel + value index
+    this.panels.splice(sourcePanelIndex+valueIndex, 0, clone);
 
 
     clone.repeatIteration = this.iteration;
     clone.repeatIteration = this.iteration;
     clone.repeatPanelId = sourcePanel.id;
     clone.repeatPanelId = sourcePanel.id;
@@ -296,37 +294,63 @@ export class DashboardModel {
     return clone;
     return clone;
   }
   }
 
 
+  getRowRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) {
+    // if first clone return source
+    if (valueIndex === 0) {
+      if (!sourcePanel.collapsed) {
+        let rowPanels = this.getRowPanels(sourcePanelIndex);
+        sourcePanel.panels = rowPanels;
+      }
+      return sourcePanel;
+    }
+
+    let clone = new PanelModel(sourcePanel.getSaveModel());
+    // for row clones we need to figure out panels under row to clone and where to insert clone
+    let rowPanels, insertPos;
+    if (sourcePanel.collapsed) {
+      rowPanels = _.cloneDeep(sourcePanel.panels);
+      clone.panels = rowPanels;
+      // insert copied row after preceding row
+      insertPos = sourcePanelIndex + valueIndex;
+    } else {
+      rowPanels = this.getRowPanels(sourcePanelIndex);
+      clone.panels = _.map(rowPanels, panel => panel.getSaveModel());
+      // insert copied row after preceding row's panels
+      insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex);
+    }
+    this.panels.splice(insertPos, 0, clone);
+
+    this.updateRepeatedPanelIds(clone);
+    return clone;
+  }
+
   getBottomYForRow() {
   getBottomYForRow() {
   }
   }
 
 
   repeatPanel(panel: PanelModel, panelIndex: number) {
   repeatPanel(panel: PanelModel, panelIndex: number) {
-    var variable = _.find(this.templating.list, {name: panel.repeat});
+    let variable = _.find(this.templating.list, {name: panel.repeat});
     if (!variable) {
     if (!variable) {
       return;
       return;
     }
     }
 
 
-    var selected;
-    if (variable.current.text === 'All') {
-      selected = variable.options.slice(1, variable.options.length);
-    } else {
-      selected = _.filter(variable.options, {selected: true});
+    if (panel.type === 'row') {
+      this.repeatRow(panel, panelIndex, variable);
+      return;
     }
     }
 
 
+    let selectedOptions = this.getSelectedVariableOptions(variable);
     let minWidth = panel.minSpan || 6;
     let minWidth = panel.minSpan || 6;
     let xPos = 0;
     let xPos = 0;
     let yPos = panel.gridPos.y;
     let yPos = panel.gridPos.y;
 
 
-    for (let index = 0; index < selected.length; index++) {
-      var option = selected[index];
-      var copy = this.getPanelRepeatClone(panel, index, panelIndex);
+    for (let index = 0; index < selectedOptions.length; index++) {
+      let option = selectedOptions[index];
+      let copy;
 
 
+      copy = this.getPanelRepeatClone(panel, index, panelIndex);
       copy.scopedVars = {};
       copy.scopedVars = {};
       copy.scopedVars[variable.name] = option;
       copy.scopedVars[variable.name] = option;
 
 
-      if (copy.type === 'row') {
-        // place row below row panels
-      }
-
       if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
       if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
         copy.gridPos.y = yPos;
         copy.gridPos.y = yPos;
         yPos += copy.gridPos.h;
         yPos += copy.gridPos.h;
@@ -334,7 +358,7 @@ export class DashboardModel {
         // set width based on how many are selected
         // set width based on how many are selected
         // assumed the repeated panels should take up full row width
         // assumed the repeated panels should take up full row width
 
 
-        copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth);
+        copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
         copy.gridPos.x = xPos;
         copy.gridPos.x = xPos;
         copy.gridPos.y = yPos;
         copy.gridPos.y = yPos;
 
 
@@ -349,6 +373,90 @@ export class DashboardModel {
     }
     }
   }
   }
 
 
+  repeatRow(panel: PanelModel, panelIndex: number, variable) {
+    let selectedOptions = this.getSelectedVariableOptions(variable);
+    let yPos = panel.gridPos.y;
+
+    function setScopedVars(panel, variableOption) {
+      panel.scopedVars = {};
+      panel.scopedVars[variable.name] = variableOption;
+    }
+
+    for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
+      let option = selectedOptions[optionIndex];
+      let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
+      setScopedVars(rowCopy, option);
+
+      let rowHeight = this.getRowHeight(rowCopy);
+      let rowPanels = rowCopy.panels || [];
+      let panelBelowIndex;
+
+      if (panel.collapsed) {
+        // For collapsed row just copy its panels and set scoped vars and proper IDs
+        _.each(rowPanels, (rowPanel, i) => {
+          setScopedVars(rowPanel, option);
+          if (optionIndex > 0) {
+            this.updateRepeatedPanelIds(rowPanel);
+          }
+        });
+        rowCopy.gridPos.y += optionIndex;
+        yPos += optionIndex;
+        panelBelowIndex = panelIndex + optionIndex + 1;
+      } else {
+        // insert after 'row' panel
+        let insertPos = panelIndex + ((rowPanels.length + 1) * optionIndex) + 1;
+        _.each(rowPanels, (rowPanel, i) => {
+          setScopedVars(rowPanel, option);
+          if (optionIndex > 0) {
+            let cloneRowPanel = new PanelModel(rowPanel);
+            this.updateRepeatedPanelIds(cloneRowPanel);
+            // For exposed row additionally set proper Y grid position and add it to dashboard panels
+            cloneRowPanel.gridPos.y += rowHeight * optionIndex;
+            this.panels.splice(insertPos+i, 0, cloneRowPanel);
+          }
+        });
+        rowCopy.panels = [];
+        rowCopy.gridPos.y += rowHeight * optionIndex;
+        yPos += rowHeight;
+        panelBelowIndex = insertPos+rowPanels.length;
+      }
+
+      // Update gridPos for panels below
+      for (let i = panelBelowIndex; i< this.panels.length; i++) {
+        this.panels[i].gridPos.y += yPos;
+      }
+    }
+  }
+
+  updateRepeatedPanelIds(panel: PanelModel) {
+    panel.repeatPanelId = panel.id;
+    panel.id = this.getNextPanelId();
+    panel.repeatIteration = this.iteration;
+    panel.repeat = null;
+    return panel;
+  }
+
+  getSelectedVariableOptions(variable) {
+    let selectedOptions;
+    if (variable.current.text === 'All') {
+      selectedOptions = variable.options.slice(1, variable.options.length);
+    } else {
+      selectedOptions = _.filter(variable.options, {selected: true});
+    }
+    return selectedOptions;
+  }
+
+  getRowHeight(rowPanel: PanelModel): number {
+    if (!rowPanel.panels || rowPanel.panels.length === 0) {
+      return 0;
+    }
+    const positions = _.map(rowPanel.panels, 'gridPos');
+    const maxPos = _.maxBy(positions, (pos) => {
+      return pos.y + pos.h;
+    });
+    return maxPos.h + 1;
+  }
+
   removePanel(panel: PanelModel) {
   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);

+ 134 - 28
public/app/features/dashboard/specs/repeat.jest.ts

@@ -1,4 +1,6 @@
+import _ from 'lodash';
 import {DashboardModel} from '../dashboard_model';
 import {DashboardModel} from '../dashboard_model';
+import { expect } from 'test/lib/common';
 
 
 jest.mock('app/core/services/context_srv', () => ({
 jest.mock('app/core/services/context_srv', () => ({
 
 
@@ -146,19 +148,19 @@ describe('given dashboard with panel repeat in vertical direction', function() {
   });
   });
 });
 });
 
 
-describe.skip('given dashboard with row repeat', function() {
-  var dashboard;
+describe('given dashboard with row repeat', function() {
+  let dashboard, dashboardJSON;
 
 
   beforeEach(function() {
   beforeEach(function() {
-    dashboard = new DashboardModel({
+    dashboardJSON = {
       panels: [
       panels: [
-        {id: 1, type: 'row',   repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}},
+        {id: 1, type: 'row',   gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'},
         {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
         {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
         {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
         {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
         {id: 4, type: 'row',   gridPos: {x: 0, y: 2, h: 1 , w: 24}},
         {id: 4, type: 'row',   gridPos: {x: 0, y: 2, h: 1 , w: 24}},
         {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
         {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
       ],
       ],
-      templating:  {
+      templating: {
         list: [{
         list: [{
           name: 'apps',
           name: 'apps',
           current: {
           current: {
@@ -172,33 +174,137 @@ describe.skip('given dashboard with row repeat', function() {
           ]
           ]
         }]
         }]
       }
       }
-    });
+    };
+    dashboard = new DashboardModel(dashboardJSON);
     dashboard.processRepeats();
     dashboard.processRepeats();
   });
   });
 
 
   it('should not repeat only row', function() {
   it('should not repeat only row', function() {
-    expect(dashboard.panels[1].type).toBe('graph');
-  });
-  //
-  // it('should set scopedVars on panels', function() {
-  //   expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}})
-  // });
-  //
-  // it.skip('should repeat row and panels below two times', function() {
-  //   expect(dashboard.panels).toMatchObject([
-  //     // first (original row)
-  //     {id: 1, type: 'row',   repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}},
-  //     {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
-  //     {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
-  //     // repeated row
-  //     {id: 1, type: 'row',   repeatPanelId: 1, gridPos: {x: 0, y: 0, h: 1 , w: 24}},
-  //     {id: 2, type: 'graph', repeatPanelId: 1, gridPos: {x: 0, y: 1, h: 1 , w: 6}},
-  //     {id: 3, type: 'graph', repeatPanelId: 1, gridPos: {x: 6, y: 1, h: 1 , w: 6}},
-  //     // row below dont touch
-  //     {id: 4, type: 'row',   gridPos: {x: 0, y: 2, h: 1 , w: 24}},
-  //     {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
-  //   ]);
-  // });
+    const panel_types = _.map(dashboard.panels, 'type');
+    expect(panel_types).toEqual([
+      'row', 'graph', 'graph',
+      'row', 'graph', 'graph',
+      'row', 'graph'
+    ]);
+  });
+
+  it('should set scopedVars for each panel', function() {
+    dashboardJSON.templating.list[0].options[2].selected = true;
+    dashboard = new DashboardModel(dashboardJSON);
+    dashboard.processRepeats();
+
+    expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}});
+    expect(dashboard.panels[4].scopedVars).toMatchObject({apps: {text: 'se2', value: 'se2'}});
+
+    const scopedVars = _.compact(_.map(dashboard.panels, (panel) => {
+      return panel.scopedVars ? panel.scopedVars.apps.value : null;
+    }));
+
+    expect(scopedVars).toEqual([
+      'se1', 'se1', 'se1',
+      'se2', 'se2', 'se2',
+      'se3', 'se3', 'se3',
+    ]);
+  });
+
+  it('should repeat only configured row', function() {
+    expect(dashboard.panels[6].id).toBe(4);
+    expect(dashboard.panels[7].id).toBe(5);
+  });
+
+  it('should repeat only row if it is collapsed', function() {
+    dashboardJSON.panels = [
+        {
+          id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24},
+          panels: [
+            {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
+            {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
+          ]
+        },
+        {id: 4, type: 'row',   gridPos: {x: 0, y: 1, h: 1 , w: 24}},
+        {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}},
+    ];
+    dashboard = new DashboardModel(dashboardJSON);
+    dashboard.processRepeats();
+
+    const panel_types = _.map(dashboard.panels, 'type');
+    expect(panel_types).toEqual([
+      'row', 'row', 'row', 'graph'
+    ]);
+    expect(dashboard.panels[0].panels).toHaveLength(2);
+    expect(dashboard.panels[1].panels).toHaveLength(2);
+  });
+
+  it('should properly repeat multiple rows', function() {
+    dashboardJSON.panels = [
+      {id: 1, type: 'row',   gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, // repeat
+      {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
+      {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
+      {id: 4, type: 'row',   gridPos: {x: 0, y: 2, h: 1 , w: 24}}, // don't touch
+      {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}},
+      {id: 6, type: 'row',   gridPos: {x: 0, y: 4, h: 1 , w: 24}, repeat: 'hosts'}, // repeat
+      {id: 7, type: 'graph', gridPos: {x: 0, y: 5, h: 1 , w: 6}},
+      {id: 8, type: 'graph', gridPos: {x: 6, y: 5, h: 1 , w: 6}}
+    ];
+    dashboardJSON.templating.list.push({
+      name: 'hosts',
+      current: {
+        text: 'backend01, backend02',
+        value: ['backend01', 'backend02']
+      },
+      options: [
+        {text: 'backend01', value: 'backend01', selected: true},
+        {text: 'backend02', value: 'backend02', selected: true},
+        {text: 'backend03', value: 'backend03', selected: false}
+      ]
+    });
+    dashboard = new DashboardModel(dashboardJSON);
+    dashboard.processRepeats();
+
+    const panel_types = _.map(dashboard.panels, 'type');
+    expect(panel_types).toEqual([
+      'row', 'graph', 'graph',
+      'row', 'graph', 'graph',
+      'row', 'graph',
+      'row', 'graph', 'graph',
+      'row', 'graph', 'graph',
+    ]);
+
+    expect(dashboard.panels[0].scopedVars['apps'].value).toBe('se1');
+    expect(dashboard.panels[1].scopedVars['apps'].value).toBe('se1');
+    expect(dashboard.panels[3].scopedVars['apps'].value).toBe('se2');
+    expect(dashboard.panels[4].scopedVars['apps'].value).toBe('se2');
+    expect(dashboard.panels[8].scopedVars['hosts'].value).toBe('backend01');
+    expect(dashboard.panels[9].scopedVars['hosts'].value).toBe('backend01');
+    expect(dashboard.panels[11].scopedVars['hosts'].value).toBe('backend02');
+    expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02');
+  });
+
+  it('should assign unique ids for repeated panels', function() {
+    dashboardJSON.panels = [
+        {
+          id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24},
+          panels: [
+            {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}},
+            {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}},
+          ]
+        },
+        {id: 4, type: 'row',   gridPos: {x: 0, y: 1, h: 1 , w: 24}},
+        {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}},
+    ];
+    dashboard = new DashboardModel(dashboardJSON);
+    dashboard.processRepeats();
+
+    const panel_ids = _.flattenDeep(_.map(dashboard.panels, (panel) => {
+      let ids = [];
+      if (panel.panels && panel.panels.length) {
+        ids = _.map(panel.panels, 'id');
+      }
+      ids.push(panel.id);
+      return ids;
+    }));
+    expect(panel_ids.length).toEqual(_.uniq(panel_ids).length);
+  });
 });
 });