diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 0e37acb201a..f4cdc26d438 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -181,6 +181,14 @@ export class DashboardModel { if (panel.id > max) { max = panel.id; } + + if (panel.collapsed) { + for (let rowPanel of panel.panels) { + if (rowPanel.id > max) { + max = rowPanel.id; + } + } + } } return max + 1; @@ -274,21 +282,11 @@ export class DashboardModel { return sourcePanel; } - var clone = new PanelModel(sourcePanel.getSaveModel()); + let clone = new PanelModel(sourcePanel.getSaveModel()); 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.repeatPanelId = sourcePanel.id; @@ -296,37 +294,63 @@ export class DashboardModel { 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() { } 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) { 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 xPos = 0; 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[variable.name] = option; - if (copy.type === 'row') { - // place row below row panels - } - if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { copy.gridPos.y = yPos; yPos += copy.gridPos.h; @@ -334,7 +358,7 @@ export class DashboardModel { // 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 / selected.length, minWidth); + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); copy.gridPos.x = xPos; 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) { var index = _.indexOf(this.panels, panel); this.panels.splice(index, 1); diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 2ad1400990c..e6db5725d3d 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,4 +1,6 @@ +import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; +import { expect } from 'test/lib/common'; 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() { - dashboard = new DashboardModel({ + dashboardJSON = { 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: 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: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, ], - templating: { + templating: { list: [{ name: 'apps', current: { @@ -172,33 +174,137 @@ describe.skip('given dashboard with row repeat', function() { ] }] } - }); + }; + dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); }); it('should not repeat only row', function() { - expect(dashboard.panels[1].type).toBe('graph'); + 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); }); - // - // 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}}, - // ]); - // }); });