From 08d470d76a94015287e66ff8c7a2d969d1febd74 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 12:51:14 +0300 Subject: [PATCH 1/7] dashboard: initial repeat row implementation --- .../app/features/dashboard/dashboard_model.ts | 78 +++++++++++++------ .../features/dashboard/specs/repeat.jest.ts | 16 ++-- 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 0e37acb201a..9481ef6d63b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -274,7 +274,7 @@ 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') { @@ -282,7 +282,7 @@ export class DashboardModel { let rowPanels = this.getRowPanels(sourcePanelIndex); clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); - // insert after preceding row's panels + // insert copied row after preceding row's panels let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); this.panels.splice(insertPos, 0, clone); } else { @@ -300,12 +300,12 @@ export class DashboardModel { } 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; + let selected; if (variable.current.text === 'All') { selected = variable.options.slice(1, variable.options.length); } else { @@ -317,38 +317,72 @@ export class DashboardModel { let yPos = panel.gridPos.y; for (let index = 0; index < selected.length; index++) { - var option = selected[index]; - var copy = this.getPanelRepeatClone(panel, index, panelIndex); + let option = selected[index]; + let copy = this.getPanelRepeatClone(panel, index, panelIndex); copy.scopedVars = {}; copy.scopedVars[variable.name] = option; if (copy.type === 'row') { // place row below row panels - } + let rowHeight = this.getRowHeight(copy); + if (rowHeight) { + copy.gridPos.y += rowHeight * index; + let rowPanels = copy.panels; + // insert after preceding row's panels + let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; + _.each(rowPanels, (rowPanel, i) => { + let cloneRowPanel = new PanelModel(rowPanel); + cloneRowPanel.id = this.getNextPanelId(); + cloneRowPanel.repeatIteration = this.iteration; + cloneRowPanel.repeatPanelId = rowPanel.id; + cloneRowPanel.repeat = null; + cloneRowPanel.gridPos.y += rowHeight * index; + this.panels.splice(insertPos+i, 0, cloneRowPanel); + }); + copy.panels = []; + yPos += rowHeight; - if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { - copy.gridPos.y = yPos; - yPos += copy.gridPos.h; + // Update gridPos for panels below + for (let i = insertPos+rowPanels.length; i< this.panels.length; i++) { + this.panels[i].gridPos.y += yPos; + } + } } 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 / selected.length, minWidth); - copy.gridPos.x = xPos; - copy.gridPos.y = yPos; - - xPos += copy.gridPos.w; - - // handle overflow by pushing down one row - if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { - xPos = 0; + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { + copy.gridPos.y = yPos; yPos += copy.gridPos.h; + } 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 / selected.length, minWidth); + copy.gridPos.x = xPos; + copy.gridPos.y = yPos; + + xPos += copy.gridPos.w; + + // handle overflow by pushing down one row + if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { + xPos = 0; + yPos += copy.gridPos.h; + } } } } } + 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..7f5d382fe9c 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; jest.mock('app/core/services/context_srv', () => ({ @@ -146,7 +147,7 @@ describe('given dashboard with panel repeat in vertical direction', function() { }); }); -describe.skip('given dashboard with row repeat', function() { +describe('given dashboard with row repeat', function() { var dashboard; beforeEach(function() { @@ -177,14 +178,19 @@ describe.skip('given dashboard with row repeat', function() { }); 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 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() { + + // it('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}}, From dd9c727e6050c515c62b5a97955b4a16c606eec5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 14:14:43 +0300 Subject: [PATCH 2/7] repeat row: handle collapsed rows --- .../app/features/dashboard/dashboard_model.ts | 71 +++++++++++++------ .../features/dashboard/specs/repeat.jest.ts | 56 ++++++++++++++- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 9481ef6d63b..cc5c743cb41 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; @@ -266,6 +274,7 @@ export class DashboardModel { this.sortPanelsByGridPos(); this.events.emit('repeats-processed'); + console.log(this.panels); } getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { @@ -279,11 +288,18 @@ export class DashboardModel { 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 copied row after preceding row's panels - let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); + let rowPanels, insertPos; + if (sourcePanel.collapsed) { + rowPanels = sourcePanel.panels; + clone.panels = _.cloneDeep(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); } else { // insert after source panel + value index @@ -324,27 +340,42 @@ export class DashboardModel { copy.scopedVars[variable.name] = option; if (copy.type === 'row') { - // place row below row panels let rowHeight = this.getRowHeight(copy); if (rowHeight) { - copy.gridPos.y += rowHeight * index; + let panelsBelowIndex; let rowPanels = copy.panels; - // insert after preceding row's panels + // insert after 'row' panel let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - _.each(rowPanels, (rowPanel, i) => { - let cloneRowPanel = new PanelModel(rowPanel); - cloneRowPanel.id = this.getNextPanelId(); - cloneRowPanel.repeatIteration = this.iteration; - cloneRowPanel.repeatPanelId = rowPanel.id; - cloneRowPanel.repeat = null; - cloneRowPanel.gridPos.y += rowHeight * index; - this.panels.splice(insertPos+i, 0, cloneRowPanel); - }); - copy.panels = []; - yPos += rowHeight; + + if (copy.collapsed) { + copy.gridPos.y += index; + yPos += index; + panelsBelowIndex = panelIndex + index + 1; + _.each(copy.panels, (panel, i) => { + panel.id = this.getNextPanelId(); + panel.repeatIteration = this.iteration; + panel.repeatPanelId = rowPanels[i].id; + panel.repeat = null; + copy.panels[i] = panel; + }); + } else { + _.each(rowPanels, (rowPanel, i) => { + let cloneRowPanel = new PanelModel(rowPanel); + cloneRowPanel.id = this.getNextPanelId(); + cloneRowPanel.repeatIteration = this.iteration; + cloneRowPanel.repeatPanelId = rowPanel.id; + cloneRowPanel.repeat = null; + cloneRowPanel.gridPos.y += rowHeight * index; + this.panels.splice(insertPos+i, 0, cloneRowPanel); + }); + copy.panels = []; + copy.gridPos.y += rowHeight * index; + yPos += rowHeight; + panelsBelowIndex = insertPos+rowPanels.length; + } // Update gridPos for panels below - for (let i = insertPos+rowPanels.length; i< this.panels.length; i++) { + for (let i = panelsBelowIndex; i< this.panels.length; i++) { this.panels[i].gridPos.y += yPos; } } diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 7f5d382fe9c..cb1da3e767a 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -148,10 +148,10 @@ describe('given dashboard with panel repeat in vertical direction', function() { }); describe('given dashboard with row repeat', function() { - var dashboard; + 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: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, @@ -173,7 +173,8 @@ describe('given dashboard with row repeat', function() { ] }] } - }); + }; + dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); }); @@ -186,6 +187,55 @@ describe('given dashboard with row repeat', function() { ]); }); + 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 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'}}) // }); From 36ef1865875b677ec3b71de822c250e10b2fc4ac Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 16:40:13 +0300 Subject: [PATCH 3/7] repeat row: expose scopedVars to row panels --- .../app/features/dashboard/dashboard_model.ts | 121 +++++++++++------- .../features/dashboard/specs/repeat.jest.ts | 17 +++ 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index cc5c743cb41..c54a9460c08 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -274,7 +274,6 @@ export class DashboardModel { this.sortPanelsByGridPos(); this.events.emit('repeats-processed'); - console.log(this.panels); } getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { @@ -286,26 +285,43 @@ export class DashboardModel { 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, insertPos; - if (sourcePanel.collapsed) { - rowPanels = sourcePanel.panels; - clone.panels = _.cloneDeep(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); + // insert after source panel + value index + this.panels.splice(sourcePanelIndex+valueIndex, 0, clone); + + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + 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; } - this.panels.splice(insertPos, 0, clone); - } else { - // insert after source panel + value index - this.panels.splice(sourcePanelIndex+valueIndex, 0, clone); + return sourcePanel; } + let clone = new PanelModel(sourcePanel.getSaveModel()); + clone.id = this.getNextPanelId(); + + // 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); + clone.repeatIteration = this.iteration; clone.repeatPanelId = sourcePanel.id; clone.repeat = null; @@ -334,32 +350,43 @@ export class DashboardModel { for (let index = 0; index < selected.length; index++) { let option = selected[index]; - let copy = this.getPanelRepeatClone(panel, index, panelIndex); + let copy; - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + if (panel.type === 'row') { + copy = this.getRowRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; - if (copy.type === 'row') { let rowHeight = this.getRowHeight(copy); - if (rowHeight) { - let panelsBelowIndex; - let rowPanels = copy.panels; - // insert after 'row' panel - let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; + // if (rowHeight) { + let panelsBelowIndex; + let rowPanels = copy.panels || []; + // insert after 'row' panel + let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - if (copy.collapsed) { - copy.gridPos.y += index; - yPos += index; - panelsBelowIndex = panelIndex + index + 1; - _.each(copy.panels, (panel, i) => { + if (copy.collapsed) { + copy.gridPos.y += index; + yPos += index; + panelsBelowIndex = panelIndex + index + 1; + + _.each(copy.panels, (panel, i) => { + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + + if (index > 0) { panel.id = this.getNextPanelId(); panel.repeatIteration = this.iteration; panel.repeatPanelId = rowPanels[i].id; panel.repeat = null; copy.panels[i] = panel; - }); - } else { - _.each(rowPanels, (rowPanel, i) => { + } + }); + } else { + _.each(rowPanels, (rowPanel, i) => { + rowPanel.scopedVars = {}; + rowPanel.scopedVars[variable.name] = option; + + if (index > 0) { let cloneRowPanel = new PanelModel(rowPanel); cloneRowPanel.id = this.getNextPanelId(); cloneRowPanel.repeatIteration = this.iteration; @@ -367,19 +394,23 @@ export class DashboardModel { cloneRowPanel.repeat = null; cloneRowPanel.gridPos.y += rowHeight * index; this.panels.splice(insertPos+i, 0, cloneRowPanel); - }); - copy.panels = []; - copy.gridPos.y += rowHeight * index; - yPos += rowHeight; - panelsBelowIndex = insertPos+rowPanels.length; - } + } + }); + copy.panels = []; + copy.gridPos.y += rowHeight * index; + yPos += rowHeight; + panelsBelowIndex = insertPos+rowPanels.length; + } - // Update gridPos for panels below - for (let i = panelsBelowIndex; i< this.panels.length; i++) { - this.panels[i].gridPos.y += yPos; - } + // Update gridPos for panels below + for (let i = panelsBelowIndex; i< this.panels.length; i++) { + this.panels[i].gridPos.y += yPos; } } else { + copy = this.getPanelRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { copy.gridPos.y = yPos; yPos += copy.gridPos.h; diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index cb1da3e767a..d44c9b1d610 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -187,6 +187,23 @@ describe('given dashboard with row repeat', function() { ]); }); + it('should set scopedVars for each panel', function() { + dashboardJSON.templating.list[0].options[2].selected = true; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + const scopedVars = _.compact(_.map(dashboard.panels, (panel) => { + if (panel.scopedVars) { + return panel.scopedVars.apps.value; + } + })); + + expect(scopedVars).toEqual([ + 'se1', 'se1', 'se1', + 'se2', 'se2', 'se2', + 'se3', 'se3', 'se3', + ]); + }); + it('should repeat only row if it is collapsed', function() { dashboardJSON.panels = [ { From 88760983ab00c903024f0d277050bdd280762abd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Nov 2017 10:41:37 +0300 Subject: [PATCH 4/7] repeat row: add more tests --- .../features/dashboard/specs/repeat.jest.ts | 83 +++++++++++++------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index d44c9b1d610..e6db5725d3d 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({ @@ -153,13 +154,13 @@ describe('given dashboard with row repeat', function() { beforeEach(function() { 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: { @@ -191,10 +192,12 @@ describe('given dashboard with row repeat', 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) => { - if (panel.scopedVars) { - return panel.scopedVars.apps.value; - } + return panel.scopedVars ? panel.scopedVars.apps.value : null; })); expect(scopedVars).toEqual([ @@ -204,6 +207,11 @@ describe('given dashboard with row repeat', function() { ]); }); + 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 = [ { @@ -227,6 +235,51 @@ describe('given dashboard with row repeat', function() { 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 = [ { @@ -252,26 +305,6 @@ describe('given dashboard with row repeat', function() { })); 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('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}}, - // ]); - // }); }); From 8593ca001d90c423afc63febe8569f4577fa810d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 1 Dec 2017 16:34:49 +0300 Subject: [PATCH 5/7] repeat row: refactor --- .../app/features/dashboard/dashboard_model.ts | 182 ++++++++++-------- 1 file changed, 97 insertions(+), 85 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index c54a9460c08..f4cdc26d438 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -305,8 +305,6 @@ export class DashboardModel { } let clone = new PanelModel(sourcePanel.getSaveModel()); - clone.id = this.getNextPanelId(); - // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels, insertPos; if (sourcePanel.collapsed) { @@ -322,9 +320,7 @@ export class DashboardModel { } this.panels.splice(insertPos, 0, clone); - clone.repeatIteration = this.iteration; - clone.repeatPanelId = sourcePanel.id; - clone.repeat = null; + this.updateRepeatedPanelIds(clone); return clone; } @@ -337,103 +333,119 @@ export class DashboardModel { return; } - let 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++) { - let option = selected[index]; + for (let index = 0; index < selectedOptions.length; index++) { + let option = selectedOptions[index]; let copy; - if (panel.type === 'row') { - copy = this.getRowRepeatClone(panel, index, panelIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + copy = this.getPanelRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; - let rowHeight = this.getRowHeight(copy); - // if (rowHeight) { - let panelsBelowIndex; - let rowPanels = copy.panels || []; - // insert after 'row' panel - let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - - if (copy.collapsed) { - copy.gridPos.y += index; - yPos += index; - panelsBelowIndex = panelIndex + index + 1; - - _.each(copy.panels, (panel, i) => { - panel.scopedVars = {}; - panel.scopedVars[variable.name] = option; - - if (index > 0) { - panel.id = this.getNextPanelId(); - panel.repeatIteration = this.iteration; - panel.repeatPanelId = rowPanels[i].id; - panel.repeat = null; - copy.panels[i] = panel; - } - }); - } else { - _.each(rowPanels, (rowPanel, i) => { - rowPanel.scopedVars = {}; - rowPanel.scopedVars[variable.name] = option; - - if (index > 0) { - let cloneRowPanel = new PanelModel(rowPanel); - cloneRowPanel.id = this.getNextPanelId(); - cloneRowPanel.repeatIteration = this.iteration; - cloneRowPanel.repeatPanelId = rowPanel.id; - cloneRowPanel.repeat = null; - cloneRowPanel.gridPos.y += rowHeight * index; - this.panels.splice(insertPos+i, 0, cloneRowPanel); - } - }); - copy.panels = []; - copy.gridPos.y += rowHeight * index; - yPos += rowHeight; - panelsBelowIndex = insertPos+rowPanels.length; - } - - // Update gridPos for panels below - for (let i = panelsBelowIndex; i< this.panels.length; i++) { - this.panels[i].gridPos.y += yPos; - } + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { + copy.gridPos.y = yPos; + yPos += copy.gridPos.h; } else { - copy = this.getPanelRepeatClone(panel, index, panelIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + // set width based on how many are selected + // assumed the repeated panels should take up full row width - if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { - copy.gridPos.y = yPos; + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); + copy.gridPos.x = xPos; + copy.gridPos.y = yPos; + + xPos += copy.gridPos.w; + + // handle overflow by pushing down one row + if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { + xPos = 0; yPos += copy.gridPos.h; - } 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 / selected.length, minWidth); - copy.gridPos.x = xPos; - copy.gridPos.y = yPos; - - xPos += copy.gridPos.w; - - // handle overflow by pushing down one row - if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { - xPos = 0; - yPos += copy.gridPos.h; - } } } } } + 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; From 66657d24b8c7763ce8911f58d5326286aa50bf7e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 4 Dec 2017 12:37:00 +0100 Subject: [PATCH 6/7] sidemenu: responsive sidemenu view for smallest breakpoint For the smallest breakpoint, expands the sidemenu to be width 100% and to be toggled on or off rather than visible all the time. --- public/app/core/components/grafana_app.ts | 10 ++ .../core/components/sidemenu/sidemenu.html | 6 + .../app/core/components/sidemenu/sidemenu.ts | 10 ++ public/app/core/services/context_srv.ts | 9 +- public/sass/components/_sidemenu.scss | 148 ++++++++++++++---- 5 files changed, 153 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index d13c6a6a00e..c60c8a20a4c 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -85,6 +85,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } }); + let sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', sidemenuOpenSmallBreakpoint); + + scope.$watch('contextSrv.sidemenuSmallBreakpoint', newVal => { + if (sidemenuOpenSmallBreakpoint !== scope.contextSrv.sidemenuSmallBreakpoint) { + sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', scope.contextSrv.sidemenuSmallBreakpoint); + } + }); + // tooltip removal fix // manage page classes var pageClass; diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 2869562c5b1..b47b7fc8265 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -2,6 +2,12 @@ + + +

 Close

+
+ +