From 08d470d76a94015287e66ff8c7a2d969d1febd74 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 12:51:14 +0300 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 d29c695d444b877f4597f2439b7b0ebaed2cdec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Dec 2017 13:38:38 +0100 Subject: [PATCH 06/16] refactor: minor refactoring of #10027 --- public/app/features/dashboard/dashboard_model.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index f4cdc26d438..10b0d0da67b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -259,16 +259,6 @@ export class DashboardModel { } } - // for (let panel of this.panels) { - // if (panel.repeat) { - // if (!cleanUpOnly) { - // this.repeatPanel(panel); - // } - // } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { - // panelsToRemove.push(panel); - // } - // } - // remove panels _.pull(this.panels, ...panelsToRemove); @@ -324,9 +314,6 @@ export class DashboardModel { return clone; } - getBottomYForRow() { - } - repeatPanel(panel: PanelModel, panelIndex: number) { let variable = _.find(this.templating.list, {name: panel.repeat}); if (!variable) { From 44ea0ff71d83ec16b543865fad1ecf4dea1a2b99 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 15:55:37 +0300 Subject: [PATCH 07/16] Move import dashboard from modal to the page --- pkg/api/index.go | 2 +- public/app/core/routes/routes.ts | 5 + public/app/features/dashboard/all.ts | 3 +- .../dashboard/dashboard_import_ctrl.ts | 163 ++++++++++++++++++ .../dashboard/partials/dashboardImport.html | 125 ++++++++++++++ ...ctrl_specs.ts => dashboard_import_ctrl.ts} | 6 +- 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 public/app/features/dashboard/dashboard_import_ctrl.ts create mode 100644 public/app/features/dashboard/partials/dashboardImport.html rename public/app/features/dashboard/specs/{dash_import_ctrl_specs.ts => dashboard_import_ctrl.ts} (93%) diff --git a/pkg/api/index.go b/pkg/api/index.go index eb9e70bb404..d2069441b81 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -95,7 +95,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, - {Text: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}, }, }) } @@ -104,6 +103,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, {Divider: true, HideFromTabs: true}, {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"}, + {Text: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 68595da4296..a965cd406d3 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -68,6 +68,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index b4f1f4e77f0..f2f2087a24e 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -15,7 +15,6 @@ import './unsavedChangesSrv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; import './upload'; -import './import/dash_import'; import './export/export_modal'; import './export_data/export_data_modal'; import './ad_hoc_filters'; @@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder'; import coreModule from 'app/core/core_module'; import {DashboardListCtrl} from './dashboard_list_ctrl'; +import {DashboardImportCtrl} from './dashboard_import_ctrl'; coreModule.controller('DashboardListCtrl', DashboardListCtrl); +coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts new file mode 100644 index 00000000000..f2b3fb90fc9 --- /dev/null +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -0,0 +1,163 @@ +import _ from 'lodash'; +import config from 'app/core/config'; + +export class DashboardImportCtrl { + navModel: any; + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + inputs: any[]; + inputsValid: boolean; + gnetUrl: string; + gnetError: string; + gnetInfo: any; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { + this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + + this.step = 1; + this.nameExists = false; + + // check gnetId in url + if ($routeParams.gnetId) { + this.gnetUrl = $routeParams.gnetId ; + this.checkGnetDashboard(); + } + } + + onUpload(dash) { + this.dash = dash; + this.dash.id = null; + this.step = 2; + this.inputs = []; + + if (this.dash.__inputs) { + for (let input of this.dash.__inputs) { + var inputModel = { + name: input.name, + label: input.label, + info: input.description, + value: input.value, + type: input.type, + pluginId: input.pluginId, + options: [] + }; + + if (input.type === 'datasource') { + this.setDatasourceOptions(input, inputModel); + } else if (!inputModel.info) { + inputModel.info = 'Specify a string constant'; + } + + this.inputs.push(inputModel); + } + } + + this.inputsValid = this.inputs.length === 0; + this.titleChanged(); + } + + setDatasourceOptions(input, inputModel) { + var sources = _.filter(config.datasources, val => { + return val.type === input.pluginId; + }); + + if (sources.length === 0) { + inputModel.info = "No data sources of type " + input.pluginName + " found"; + } else if (!inputModel.info) { + inputModel.info = "Select a " + input.pluginName + " data source"; + } + + inputModel.options = sources.map(val => { + return {text: val.name, value: val.name}; + }); + } + + inputValueChanged() { + this.inputsValid = true; + for (let input of this.inputs) { + if (!input.value) { + this.inputsValid = false; + } + } + } + + titleChanged() { + this.backendSrv.search({query: this.dash.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.dash.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } + + saveDashboard() { + var inputs = this.inputs.map(input => { + return { + name: input.name, + type: input.type, + pluginId: input.pluginId, + value: input.value + }; + }); + + return this.backendSrv.post('api/dashboards/import', { + dashboard: this.dash, + overwrite: true, + inputs: inputs + }).then(res => { + this.$location.url('dashboard/' + res.importedUri); + this.$scope.dismiss(); + }); + } + + loadJsonText() { + try { + this.parseError = ''; + var dash = JSON.parse(this.jsonText); + this.onUpload(dash); + } catch (err) { + console.log(err); + this.parseError = err.message; + return; + } + } + + checkGnetDashboard() { + this.gnetError = ''; + + var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + var dashboardId; + + if (match && match[1]) { + dashboardId = match[1]; + } else if (match && match[2]) { + dashboardId = match[2]; + } else { + this.gnetError = 'Could not find dashboard'; + } + + return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { + this.gnetInfo = res; + // store reference to grafana.com + res.json.gnetId = res.id; + this.onUpload(res.json); + }).catch(err => { + err.isHandled = true; + this.gnetError = err.data.message || err; + }); + } + + back() { + this.gnetUrl = ''; + this.step = 1; + this.gnetError = ''; + this.gnetInfo = ''; + } +} diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html new file mode 100644 index 00000000000..be43078d32a --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -0,0 +1,125 @@ + + +
+
+ +
+ +
+ +
Grafana.com Dashboard
+ +
+
+ +
+
+ +
+
+ +
Or paste JSON
+ +
+
+ +
+ + + + {{ctrl.parseError}} + +
+
+ +
+
+

+ Importing Dashboard from + Grafana.com +

+ +
+ + +
+
+ + +
+
+ +

+ Options +

+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+ + + Cancel +
+ +
+
diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts similarity index 93% rename from public/app/features/dashboard/specs/dash_import_ctrl_specs.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.ts index c541aca34b2..e78109b01a0 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts @@ -1,9 +1,9 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; -import {DashImportCtrl} from 'app/features/dashboard/import/dash_import'; +import {DashboardImportCtrl} from '../dashboard_import_ctrl'; import config from 'app/core/config'; -describe('DashImportCtrl', function() { +describe('DashboardImportCtrl', function() { var ctx: any = {}; var backendSrv = { search: sinon.stub().returns(Promise.resolve([])), @@ -15,7 +15,7 @@ describe('DashImportCtrl', function() { beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { ctx.$q = $q; ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashImportCtrl, { + ctx.ctrl = $controller(DashboardImportCtrl, { $scope: ctx.scope, backendSrv: backendSrv, }); From 4403d919c10457fb5d85a52303341255297fe18f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 16:51:37 +0300 Subject: [PATCH 08/16] move DashboardImportCtrl tests to jest --- ..._ctrl.ts => dashboard_import_ctrl.jest.ts} | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) rename public/app/features/dashboard/specs/{dashboard_import_ctrl.ts => dashboard_import_ctrl.jest.ts} (57%) diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts similarity index 57% rename from public/app/features/dashboard/specs/dashboard_import_ctrl.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index e78109b01a0..3270bb5a732 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -1,25 +1,24 @@ -import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; - import {DashboardImportCtrl} from '../dashboard_import_ctrl'; -import config from 'app/core/config'; +import config from '../../../core/config'; describe('DashboardImportCtrl', function() { var ctx: any = {}; - var backendSrv = { - search: sinon.stub().returns(Promise.resolve([])), - get: sinon.stub() - }; - beforeEach(angularMocks.module('grafana.core')); + let navModelSrv; + let backendSrv; - beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { - ctx.$q = $q; - ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashboardImportCtrl, { - $scope: ctx.scope, - backendSrv: backendSrv, - }); - })); + beforeEach(() => { + navModelSrv = { + getNav: () => {} + }; + + backendSrv = { + search: jest.fn().mockReturnValue(Promise.resolve([])), + get: jest.fn() + }; + + ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {}); + }); describe('when uploading json', function() { beforeEach(function() { @@ -37,13 +36,13 @@ describe('DashboardImportCtrl', function() { }); it('should build input model', function() { - expect(ctx.ctrl.inputs.length).to.eql(1); - expect(ctx.ctrl.inputs[0].name).to.eql('ds'); - expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); + expect(ctx.ctrl.inputs.length).toBe(1); + expect(ctx.ctrl.inputs[0].name).toBe('ds'); + expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); it('should set inputValid to false', function() { - expect(ctx.ctrl.inputsValid).to.eql(false); + expect(ctx.ctrl.inputsValid).toBe(false); }); }); @@ -51,7 +50,7 @@ describe('DashboardImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -60,7 +59,7 @@ describe('DashboardImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); @@ -68,7 +67,7 @@ describe('DashboardImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = '2342'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -77,10 +76,8 @@ describe('DashboardImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); }); - - From 4d7ff4de150a0cafe8349ff9d5ea72ef26c9c9d5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 18:00:22 +0300 Subject: [PATCH 09/16] move import menu item to the original place --- pkg/api/index.go | 3 ++- public/app/core/routes/routes.ts | 10 +++++----- public/app/features/dashboard/dashboard_import_ctrl.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index d2069441b81..c1945906624 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -90,11 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Create", + Id: "create", Icon: "fa fa-fw fa-plus", Url: "#", Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, + {Text: "Import", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } @@ -103,7 +105,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, {Divider: true, HideFromTabs: true}, {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"}, - {Text: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index a965cd406d3..d3ef3d51477 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/dashboard/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/datasources', { templateUrl: 'public/app/features/plugins/partials/ds_list.html', controller : 'DataSourcesCtrl', @@ -68,11 +73,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/import', { - templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', - controller : 'DashboardImportCtrl', - controllerAs: 'ctrl', - }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index f2b3fb90fc9..9a630681145 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -16,7 +16,7 @@ export class DashboardImportCtrl { /** @ngInject */ constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { - this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + this.navModel = navModelSrv.getNav('create', 'import'); this.step = 1; this.nameExists = false; From ba3a81aba5f50234dbdf1de62c211ac19cd08ad9 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Dec 2017 16:18:46 +0100 Subject: [PATCH 10/16] ux: Add CTA for empty lists --- public/app/core/angular_wrappers.ts | 2 + .../EmptyListCTA/EmptyListCTA.jest.tsx | 22 +++++ .../components/EmptyListCTA/EmptyListCTA.tsx | 34 ++++++++ .../__snapshots__/EmptyListCTA.jest.tsx.snap | 38 +++++++++ .../features/plugins/partials/ds_list.html | 83 +++++++++++-------- public/sass/_grafana.scss | 1 + public/sass/_variables.scss | 7 +- public/sass/base/_type.scss | 2 + public/sass/components/_buttons.scss | 11 +++ public/sass/components/_empty_list_cta.scss | 21 +++++ 10 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx create mode 100644 public/app/core/components/EmptyListCTA/EmptyListCTA.tsx create mode 100644 public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap create mode 100644 public/sass/components/_empty_list_cta.scss diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 00181736119..3880be7de8d 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,10 +1,12 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; import PageHeader from './components/PageHeader'; +import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]); + react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); } diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx new file mode 100644 index 00000000000..d62ae892a0a --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import EmptyListCTA from './EmptyListCTA'; + +const model = { + title: 'Title', + buttonIcon: 'ga css class', + buttonLink: 'http://url/to/destination', + buttonTitle: 'Click me', + proTip: 'This is a tip', + proTipLink: 'http://url/to/tip/destination', + proTipLinkTitle: 'Learn more', + proTipTarget: '_blank' +}; + +describe('CollorPalette', () => { + + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx new file mode 100644 index 00000000000..a5a78a63bf1 --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; + +export interface IProps { + model: any; +} + +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget + } = this.props.model; + return ( +
+
{title}
+ {buttonTitle} +
+ ProTip: {proTip} + {proTipLinkTitle} +
+
+ ); + } +} + +export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap new file mode 100644 index 00000000000..0598d7ecd4e --- /dev/null +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollorPalette renders correctly 1`] = ` +
+
+ Title +
+ + + Click me + +
+ + ProTip: + This is a tip + + Learn more + +
+
+`; diff --git a/public/app/features/plugins/partials/ds_list.html b/public/app/features/plugins/partials/ds_list.html index 0669beb4129..91d156de317 100644 --- a/public/app/features/plugins/partials/ds_list.html +++ b/public/app/features/plugins/partials/ds_list.html @@ -1,46 +1,57 @@
- +
- No data sources defined +
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index af4a97dc1e6..614089d3121 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -86,6 +86,7 @@ @import "components/dashboard_grid"; @import "components/dashboard_list"; @import "components/page_header"; +@import "components/empty_list_cta"; // PAGES diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index ec1460ac584..62091acdab5 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -218,8 +218,11 @@ $btn-font-weight: 500 !default; $btn-padding-x-sm: .5rem !default; $btn-padding-y-sm: .25rem !default; -$btn-padding-x-lg: 1.5rem !default; -$btn-padding-y-lg: .75rem !default; +$btn-padding-x-lg: 21px !default; +$btn-padding-y-lg: 11px !default; + +$btn-padding-x-xl: 21px !default; +$btn-padding-y-xl: 11px !default; $btn-border-radius: 3px; diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index fa6539a7175..1d2fed48830 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -48,6 +48,8 @@ a.text-success:hover, a.text-success:focus { color: darken($success-text-color, 10%); } a { cursor: pointer; } +.text-link { text-decoration: underline; } + a:focus { outline:0 none !important; } diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index cea04c28cd7..391acdc5e87 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -51,10 +51,21 @@ // Button Sizes // -------------------------------------------------- +// XLarge +.btn-xlarge { + @include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $btn-border-radius); + font-weight: normal; + padding-bottom: $btn-padding-y-xl - 3; + .gicon { + font-size: 31px; + margin-right: 1rem; + } +} // Large .btn-large { @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius); + font-weight: normal; } .btn-small { diff --git a/public/sass/components/_empty_list_cta.scss b/public/sass/components/_empty_list_cta.scss new file mode 100644 index 00000000000..a8c235d83db --- /dev/null +++ b/public/sass/components/_empty_list_cta.scss @@ -0,0 +1,21 @@ +.empty-list-cta { + background-color: $search-filter-box-bg; + text-align: center; +} + +.empty-list-cta__title { + padding-bottom: 30px; + font-style: italic; +} + +.empty-list-cta__button { + margin-bottom: 50px; +} + +.empty-list-cta__pro-tip { + padding-bottom: 20px; +} + +.empty-list-cta__pro-tip-link { + margin-left: 5px; +} \ No newline at end of file From 9be6f9734d1efcc0880866fc4bc654416aea0cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Dec 2017 16:29:46 +0100 Subject: [PATCH 11/16] ux: updated padding --- public/sass/components/_tabbed_view.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index dac7408d70f..d2c9b24d2a0 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -26,7 +26,7 @@ .tabbed-view-panel-title { float: left; - padding-top: 1rem; + padding-top: 9px; margin: 0 2rem 0 0; } From afc036492c62518f883c0b424b696b8cec893fba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Dec 2017 13:46:58 +0300 Subject: [PATCH 12/16] dashboard: migrations for repeat rows (#10070) --- .../features/dashboard/dashboard_migration.ts | 5 ++-- .../specs/dashboard_migration.jest.ts | 26 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 6cc3e37f2f8..95a70cbfcab 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -383,8 +383,8 @@ export class DashboardMigrator { return; } - // Add special "row" panels if even one row is collapsed or has visible title - const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle); + // Add special "row" panels if even one row is collapsed, repeated or has visible title + const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle || row.repeat); for (let row of old.rows) { let height: any = row.height || DEFAULT_ROW_HEIGHT; @@ -398,6 +398,7 @@ export class DashboardMigrator { rowPanel.type = 'row'; rowPanel.title = row.title; rowPanel.collapsed = row.collapse; + rowPanel.repeat = row.repeat; rowPanel.panels = []; rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight}; rowPanelModel = new PanelModel(rowPanel); diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 2c8b5ebcd50..dfdcb7601a4 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { DashboardModel } from '../dashboard_model'; import { PanelModel } from '../panel_model'; import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); @@ -315,12 +316,33 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); + + it('should add repeated row if repeat set', function() { + model.rows = [ + createRow({showTitle: true, title: "Row", height: 8, repeat: "server"}, [[6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, + {x: 0, y: 1, w: 12, h: 8}, + {x: 0, y: 9, w: 24, h: 8}, + {x: 0, y: 10, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + expect(dashboard.panels[0].repeat).toBe("server"); + expect(dashboard.panels[1].repeat).toBeUndefined(); + expect(dashboard.panels[2].repeat).toBeUndefined(); + expect(dashboard.panels[3].repeat).toBeUndefined(); + }); }); }); function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; - let {collapse, height, showTitle, title} = options; + let {collapse, height, showTitle, title, repeat} = options; height = height * PANEL_HEIGHT_STEP; let panels = []; _.each(panelDescriptions, panelDesc => { @@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) { } panels.push(panel); }); - let row = {collapse, height, showTitle, title, panels}; + let row = {collapse, height, showTitle, title, panels, repeat}; return row; } From 90da964a144abc4a601fdca053d2f43bd146a57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Dec 2017 12:19:01 +0100 Subject: [PATCH 13/16] refactoring PR #10068 --- pkg/api/index.go | 2 +- .../dashboard/import/dash_import.html | 138 -------------- .../features/dashboard/import/dash_import.ts | 176 ------------------ .../dashboard/partials/dashboardImport.html | 7 +- public/app/features/dashboard/upload.ts | 2 +- 5 files changed, 6 insertions(+), 319 deletions(-) delete mode 100644 public/app/features/dashboard/import/dash_import.html delete mode 100644 public/app/features/dashboard/import/dash_import.ts diff --git a/pkg/api/index.go b/pkg/api/index.go index c1945906624..31e413a7ca3 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -96,7 +96,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, - {Text: "Import", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, + {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html deleted file mode 100644 index c617c752366..00000000000 --- a/public/app/features/dashboard/import/dash_import.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - diff --git a/public/app/features/dashboard/import/dash_import.ts b/public/app/features/dashboard/import/dash_import.ts deleted file mode 100644 index 8f3de6adc60..00000000000 --- a/public/app/features/dashboard/import/dash_import.ts +++ /dev/null @@ -1,176 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; -import _ from 'lodash'; - -export class DashImportCtrl { - step: number; - jsonText: string; - parseError: string; - nameExists: boolean; - dash: any; - inputs: any[]; - inputsValid: boolean; - gnetUrl: string; - gnetError: string; - gnetInfo: any; - - /** @ngInject */ - constructor(private backendSrv, private $location, private $scope, $routeParams) { - this.step = 1; - this.nameExists = false; - - // check gnetId in url - if ($routeParams.gnetId) { - this.gnetUrl = $routeParams.gnetId ; - this.checkGnetDashboard(); - } - } - - onUpload(dash) { - this.dash = dash; - this.dash.id = null; - this.step = 2; - this.inputs = []; - - if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { - var inputModel = { - name: input.name, - label: input.label, - info: input.description, - value: input.value, - type: input.type, - pluginId: input.pluginId, - options: [] - }; - - if (input.type === 'datasource') { - this.setDatasourceOptions(input, inputModel); - } else if (!inputModel.info) { - inputModel.info = 'Specify a string constant'; - } - - this.inputs.push(inputModel); - } - } - - this.inputsValid = this.inputs.length === 0; - this.titleChanged(); - } - - setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { - return val.type === input.pluginId; - }); - - if (sources.length === 0) { - inputModel.info = "No data sources of type " + input.pluginName + " found"; - } else if (!inputModel.info) { - inputModel.info = "Select a " + input.pluginName + " data source"; - } - - inputModel.options = sources.map(val => { - return {text: val.name, value: val.name}; - }); - } - - inputValueChanged() { - this.inputsValid = true; - for (let input of this.inputs) { - if (!input.value) { - this.inputsValid = false; - } - } - } - - titleChanged() { - this.backendSrv.search({query: this.dash.title}).then(res => { - this.nameExists = false; - for (let hit of res) { - if (this.dash.title === hit.title) { - this.nameExists = true; - break; - } - } - }); - } - - saveDashboard() { - var inputs = this.inputs.map(input => { - return { - name: input.name, - type: input.type, - pluginId: input.pluginId, - value: input.value - }; - }); - - return this.backendSrv.post('api/dashboards/import', { - dashboard: this.dash, - overwrite: true, - inputs: inputs - }).then(res => { - this.$location.url('dashboard/' + res.importedUri); - this.$scope.dismiss(); - }); - } - - loadJsonText() { - try { - this.parseError = ''; - var dash = JSON.parse(this.jsonText); - this.onUpload(dash); - } catch (err) { - console.log(err); - this.parseError = err.message; - return; - } - } - - checkGnetDashboard() { - this.gnetError = ''; - - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; - - if (match && match[1]) { - dashboardId = match[1]; - } else if (match && match[2]) { - dashboardId = match[2]; - } else { - this.gnetError = 'Could not find dashboard'; - } - - return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { - this.gnetInfo = res; - // store reference to grafana.com - res.json.gnetId = res.id; - this.onUpload(res.json); - }).catch(err => { - err.isHandled = true; - this.gnetError = err.data.message || err; - }); - } - - back() { - this.gnetUrl = ''; - this.step = 1; - this.gnetError = ''; - this.gnetInfo = ''; - } - -} - -export function dashImportDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/import/dash_import.html', - controller: DashImportCtrl, - bindToController: true, - controllerAs: 'ctrl', - }; -} - -coreModule.directive('dashImport', dashImportDirective); diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html index be43078d32a..b740b8f38bc 100644 --- a/public/app/features/dashboard/partials/dashboardImport.html +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -3,7 +3,8 @@
-
+ +
@@ -112,10 +113,10 @@
- - Cancel diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 45a1f1b4a0c..61cca0d4216 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -