diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 0ce89137284..f2d8fa5939e 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -230,10 +230,6 @@ export class DashboardModel { } cleanUpRepeats() { - this.processRepeats(true); - } - - processRepeats(cleanUpOnly?: boolean) { if (this.snapshot || this.templating.list.length === 0) { return; } @@ -248,11 +244,7 @@ export class DashboardModel { for (let i = 0; i < this.panels.length; i++) { let panel = this.panels[i]; - if (panel.repeat) { - if (!cleanUpOnly) { - this.repeatPanel(panel, i); - } - } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) { panelsToRemove.push(panel); } } @@ -264,6 +256,26 @@ export class DashboardModel { this.events.emit('repeats-processed'); } + processRepeats(cleanUpOnly?: boolean) { + if (this.snapshot || this.templating.list.length === 0) { + return; + } + + this.cleanUpRepeats(); + + this.iteration = (this.iteration || new Date().getTime()) + 1; + + for (let i = 0; i < this.panels.length; i++) { + let panel = this.panels[i]; + if (panel.repeat) { + this.repeatPanel(panel, i); + } + } + + this.sortPanelsByGridPos(); + this.events.emit('repeats-processed'); + } + getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { // if first clone return source if (valueIndex === 0) { @@ -282,21 +294,21 @@ export class DashboardModel { return clone; } - getRowRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { + getRowRepeatClone(sourceRowPanel, valueIndex, sourcePanelIndex) { // if first clone return source if (valueIndex === 0) { - if (!sourcePanel.collapsed) { + if (!sourceRowPanel.collapsed) { let rowPanels = this.getRowPanels(sourcePanelIndex); - sourcePanel.panels = rowPanels; + sourceRowPanel.panels = rowPanels; } - return sourcePanel; + return sourceRowPanel; } - let clone = new PanelModel(sourcePanel.getSaveModel()); + let clone = new PanelModel(sourceRowPanel.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); + if (sourceRowPanel.collapsed) { + rowPanels = _.cloneDeep(sourceRowPanel.panels); clone.panels = rowPanels; // insert copied row after preceding row insertPos = sourcePanelIndex + valueIndex; @@ -333,7 +345,7 @@ export class DashboardModel { let copy; copy = this.getPanelRepeatClone(panel, index, panelIndex); - copy.scopedVars = {}; + copy.scopedVars = copy.scopedVars || {}; copy.scopedVars[variable.name] = option; if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { @@ -342,7 +354,6 @@ export class DashboardModel { } else { // set width based on how many are selected // assumed the repeated panels should take up full row width - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); copy.gridPos.x = xPos; copy.gridPos.y = yPos; @@ -363,7 +374,7 @@ export class DashboardModel { let yPos = panel.gridPos.y; function setScopedVars(panel, variableOption) { - panel.scopedVars = {}; + panel.scopedVars = panel.scopedVars || {}; panel.scopedVars[variable.name] = variableOption; } @@ -381,7 +392,7 @@ export class DashboardModel { _.each(rowPanels, (rowPanel, i) => { setScopedVars(rowPanel, option); if (optionIndex > 0) { - this.updateRepeatedPanelIds(rowPanel); + this.updateRepeatedPanelIds(rowPanel, true); } }); rowCopy.gridPos.y += optionIndex; @@ -394,7 +405,7 @@ export class DashboardModel { setScopedVars(rowPanel, option); if (optionIndex > 0) { let cloneRowPanel = new PanelModel(rowPanel); - this.updateRepeatedPanelIds(cloneRowPanel); + this.updateRepeatedPanelIds(cloneRowPanel, true); // 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); @@ -413,11 +424,15 @@ export class DashboardModel { } } - updateRepeatedPanelIds(panel: PanelModel) { + updateRepeatedPanelIds(panel: PanelModel, repeatedByRow?: boolean) { panel.repeatPanelId = panel.id; panel.id = this.getNextPanelId(); panel.repeatIteration = this.iteration; - panel.repeat = null; + if (repeatedByRow) { + panel.repeatedByRow = true; + } else { + panel.repeat = null; + } return panel; } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 6173fa9ffee..daca8e60f8e 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -26,6 +26,7 @@ export class PanelModel { repeatIteration?: number; repeatPanelId?: number; repeatDirection?: string; + repeatedByRow?: boolean; minSpan?: number; collapsed?: boolean; panels?: any; diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 5c297992a0b..fbf1f836191 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -382,3 +382,132 @@ describe('given dashboard with row repeat', function() { expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); }); }); + +describe('given dashboard with row and panel repeat', () => { + let dashboard, dashboardJSON; + + beforeEach(() => { + dashboardJSON = { + panels: [ + { + id: 1, + type: 'row', + repeat: 'region', + gridPos: { x: 0, y: 0, h: 1, w: 24 }, + }, + { id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 1, w: 6 } }, + ], + templating: { + list: [ + { + name: 'region', + current: { + text: 'reg1, reg2', + value: ['reg1', 'reg2'], + }, + options: [ + { text: 'reg1', value: 'reg1', selected: true }, + { text: 'reg2', value: 'reg2', selected: true }, + { text: 'reg3', value: 'reg3', selected: false }, + ], + }, + { + name: 'app', + current: { + text: 'se1, se2', + value: ['se1', 'se2'], + }, + options: [ + { text: 'se1', value: 'se1', selected: true }, + { text: 'se2', value: 'se2', selected: true }, + { text: 'se3', value: 'se3', selected: false }, + ], + }, + ], + }, + }; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + }); + + it('should repeat row and panels for each row', () => { + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); + }); + + it('should clean up old repeated panels', () => { + dashboardJSON.panels = [ + { + id: 1, + type: 'row', + repeat: 'region', + gridPos: { x: 0, y: 0, h: 1, w: 24 }, + }, + { id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 1, w: 6 } }, + { id: 3, type: 'graph', repeatPanelId: 2, repeatIteration: 101, gridPos: { x: 7, y: 1, h: 1, w: 6 } }, + { + id: 11, + type: 'row', + repeatPanelId: 1, + repeatIteration: 101, + gridPos: { x: 0, y: 2, h: 1, w: 24 }, + }, + { id: 12, type: 'graph', repeatPanelId: 2, repeatIteration: 101, gridPos: { x: 0, y: 3, h: 1, w: 6 } }, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); + }); + + it('should set scopedVars for each row', () => { + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels[0].scopedVars).toMatchObject({ + region: { text: 'reg1', value: 'reg1' }, + }); + expect(dashboard.panels[3].scopedVars).toMatchObject({ + region: { text: 'reg2', value: 'reg2' }, + }); + }); + + it('should set panel-repeat variable for each panel', () => { + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels[1].scopedVars).toMatchObject({ + app: { text: 'se1', value: 'se1' }, + }); + expect(dashboard.panels[2].scopedVars).toMatchObject({ + app: { text: 'se2', value: 'se2' }, + }); + + expect(dashboard.panels[4].scopedVars).toMatchObject({ + app: { text: 'se1', value: 'se1' }, + }); + expect(dashboard.panels[5].scopedVars).toMatchObject({ + app: { text: 'se2', value: 'se2' }, + }); + }); + + it('should set row-repeat variable for each panel', () => { + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels[1].scopedVars).toMatchObject({ + region: { text: 'reg1', value: 'reg1' }, + }); + expect(dashboard.panels[2].scopedVars).toMatchObject({ + region: { text: 'reg1', value: 'reg1' }, + }); + + expect(dashboard.panels[4].scopedVars).toMatchObject({ + region: { text: 'reg2', value: 'reg2' }, + }); + expect(dashboard.panels[5].scopedVars).toMatchObject({ + region: { text: 'reg2', value: 'reg2' }, + }); + }); +});