diff --git a/public/app/core/core.ts b/public/app/core/core.ts index f78ed40d3cbc..ed845599991e 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -51,8 +51,10 @@ import {userGroupPicker} from './components/user_group_picker'; import {geminiScrollbar} from './components/scroll/scroll'; import {gfPageDirective} from './components/gf_page'; import {orgSwitcher} from './components/org_switcher'; +import {profiler} from './profiler'; export { + profiler, arrayJoin, coreModule, grafanaAppDirective, diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 47c459b0f13e..0e19270723df 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -20,7 +20,6 @@ export class DashboardCtrl implements PanelContainer { private alertingSrv, private dashboardSrv, private unsavedChangesSrv, - private dynamicDashboardSrv, private dashboardViewStateSrv, private panelLoader) { // temp hack due to way dashboards are loaded @@ -57,10 +56,9 @@ export class DashboardCtrl implements PanelContainer { .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) // continue .finally(() => { - this.dashboard = dashboard; - this.dynamicDashboardSrv.init(dashboard); - this.dynamicDashboardSrv.process(); + this.dashboard = dashboard; + this.dashboard.processRepeats(); this.unsavedChangesSrv.init(dashboard, this.$scope); @@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer { } templateVariableUpdated() { - this.dynamicDashboardSrv.process(); + this.dashboard.processRepeats(); } setWindowTitleAndTheme() { diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 2cee50fd3a16..017fddcafa32 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -2,7 +2,7 @@ import moment from 'moment'; import _ from 'lodash'; import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; -import {Emitter, contextSrv, appEvents} from 'app/core/core'; +import {Emitter, contextSrv} from 'app/core/core'; import {DashboardRow} from './row/row_model'; import {PanelModel} from './panel_model'; import sortByKeys from 'app/core/utils/sort_by_keys'; @@ -34,12 +34,19 @@ export class DashboardModel { revision: number; links: any; gnetId: any; - meta: any; - events: any; editMode: boolean; folderId: number; panels: PanelModel[]; + // ------------------ + // not persisted + // ------------------ + + // repeat process cycles + iteration: number; + meta: any; + events: Emitter; + static nonPersistedProperties: {[str: string]: boolean} = { "events": true, "meta": true, @@ -193,7 +200,12 @@ export class DashboardModel { this.panels.unshift(new PanelModel(panel)); - // make sure it's sorted by pos + this.sortPanelsByGridPos(); + + this.events.emit('panel-added', panel); + } + + private sortPanelsByGridPos() { this.panels.sort(function(panelA, panelB) { if (panelA.gridPos.y === panelB.gridPos.y) { return panelA.gridPos.x - panelB.gridPos.x; @@ -201,33 +213,86 @@ export class DashboardModel { return panelA.gridPos.y - panelB.gridPos.y; } }); + } - this.events.emit('panel-added', panel); + cleanUpRepeats() { + this.processRepeats(true); } - removePanel(panel, ask?) { - // confirm deletion - if (ask !== false) { - var text2, confirmText; - if (panel.alert) { - text2 = "Panel includes an alert rule, removing panel will also remove alert rule"; - confirmText = "YES"; - } + processRepeats(cleanUpOnly?: boolean) { + if (this.snapshot || this.templating.list.length === 0) { + return; + } + + this.iteration = (this.iteration || new Date().getTime()) + 1; - appEvents.emit('confirm-modal', { - title: 'Remove Panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'fa-trash', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => { - this.removePanel(panel, false); + let panelsToRemove = []; + + for (let panel of this.panels) { + if (panel.repeat) { + if (!cleanUpOnly) { + this.repeatPanel(panel); } - }); - return; + } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + panelsToRemove.push(panel); + } } + // remove panels + _.pull(this.panels, ...panelsToRemove); + + this.sortPanelsByGridPos(); + this.events.emit('repeats-processed'); + } + + getRepeatClone(sourcePanel, index) { + // if first clone return source + if (index === 0) { + return sourcePanel; + } + + var clone = new PanelModel(sourcePanel.getSaveModel()); + clone.id = this.getNextPanelId(); + this.panels.push(clone); + + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + return clone; + } + + repeatPanel(panel: PanelModel) { + var variable = _.find(this.templating.list, {name: panel.repeat}); + if (!variable) { return; } + + var selected; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + for (let index = 0; index < selected.length; index++) { + var option = selected[index]; + var copy = this.getRepeatClone(panel, index); + + copy.scopedVars = copy.scopedVars || {}; + copy.scopedVars[variable.name] = option; + + // souce panel uses original possition + if (index === 0) { + continue; + } + + if (panel.repeatDirection === 'Y') { + copy.gridPos.y = panel.gridPos.y + (panel.gridPos.h*index); + } else { + copy.gridPos.x = panel.gridPos.x + (panel.gridPos.w*index); + } + } + } + + removePanel(panel: PanelModel) { var index = _.indexOf(this.panels, panel); this.panels.splice(index, 1); this.events.emit('panel-removed', panel); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 6aecb9034798..39630e31a58e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -69,6 +69,7 @@ export class DashboardGrid extends React.Component { this.dashboard = this.panelContainer.getDashboard(); this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); + this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this)); } diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts deleted file mode 100644 index ed59e77c117b..000000000000 --- a/public/app/features/dashboard/dynamic_dashboard_srv.ts +++ /dev/null @@ -1,192 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; - -import coreModule from 'app/core/core_module'; -import {DashboardRow} from './row/row_model'; - -export class DynamicDashboardSrv { - iteration: number; - dashboard: any; - variables: any; - - init(dashboard) { - this.dashboard = dashboard; - this.variables = dashboard.templating.list; - } - - process(options?) { - if (this.dashboard.snapshot || this.variables.length === 0) { - return; - } - - this.iteration = (this.iteration || new Date().getTime()) + 1; - - options = options || {}; - var cleanUpOnly = options.cleanUpOnly; - var i, j, row, panel; - - if (this.dashboard.rows) { - // cleanup scopedVars - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; - delete row.scopedVars; - - for (j = 0; j < row.panels.length; j++) { - delete row.panels[j].scopedVars; - } - } - - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; - - // handle row repeats - if (row.repeat) { - if (!cleanUpOnly) { - this.repeatRow(row, i); - } - } else if (row.repeatRowId && row.repeatIteration !== this.iteration) { - // clean up old left overs - this.dashboard.removeRow(row, true); - i = i - 1; - continue; - } - - // repeat panels - for (j = 0; j < row.panels.length; j++) { - panel = row.panels[j]; - if (panel.repeat) { - if (!cleanUpOnly) { - this.repeatPanel(panel, row); - } - } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { - // clean up old left overs - row.panels = _.without(row.panels, panel); - j = j - 1; - } - } - - row.panelSpanChanged(); - } - } - } - - // returns a new row clone or reuses a clone from previous iteration - getRowClone(sourceRow, repeatIndex, sourceRowIndex) { - if (repeatIndex === 0) { - return sourceRow; - } - - var i, panel, row, copy; - var sourceRowId = sourceRowIndex + 1; - - // look for row to reuse - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; - if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { - copy = row; - copy.copyPropertiesFromRowSource(sourceRow); - break; - } - } - - if (!copy) { - var modelCopy = angular.copy(sourceRow.getSaveModel()); - copy = new DashboardRow(modelCopy); - this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy); - - // set new panel ids - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.id = this.dashboard.getNextPanelId(); - } - } - - copy.repeat = null; - copy.repeatRowId = sourceRowId; - copy.repeatIteration = this.iteration; - return copy; - } - - // returns a new row clone or reuses a clone from previous iteration - repeatRow(row, rowIndex) { - var variable = _.find(this.variables, {name: row.repeat}); - if (!variable) { - return; - } - - var selected, copy, i, panel; - if (variable.current.text === 'All') { - selected = variable.options.slice(1, variable.options.length); - } else { - selected = _.filter(variable.options, {selected: true}); - } - - _.each(selected, (option, index) => { - copy = this.getRowClone(row, index, rowIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; - - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.scopedVars = {}; - panel.scopedVars[variable.name] = option; - } - }); - } - - getPanelClone(sourcePanel, row, index) { - // if first clone return source - if (index === 0) { - return sourcePanel; - } - - var i, tmpId, panel, clone; - - // first try finding an existing clone to use - for (i = 0; i < row.panels.length; i++) { - panel = row.panels[i]; - if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { - clone = panel; - break; - } - } - - if (!clone) { - clone = { id: this.dashboard.getNextPanelId() }; - row.panels.push(clone); - } - - // save id - tmpId = clone.id; - // copy properties from source - angular.copy(sourcePanel, clone); - // restore id - clone.id = tmpId; - clone.repeatIteration = this.iteration; - clone.repeatPanelId = sourcePanel.id; - clone.repeat = null; - return clone; - } - - repeatPanel(panel, row) { - var variable = _.find(this.variables, {name: panel.repeat}); - if (!variable) { return; } - - var selected; - if (variable.current.text === 'All') { - selected = variable.options.slice(1, variable.options.length); - } else { - selected = _.filter(variable.options, {selected: true}); - } - - _.each(selected, (option, index) => { - var copy = this.getPanelClone(panel, row, index); - copy.span = Math.max(12 / selected.length, panel.minSpan || 4); - copy.scopedVars = copy.scopedVars || {}; - copy.scopedVars[variable.name] = option; - }); - } -} - -coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv); - diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index 3cb4393f4aab..d78883fccad7 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -1,29 +1,24 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; -import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; +import {DashboardModel} from '../dashboard_model'; export class DashboardExporter { constructor(private datasourceSrv) { } - makeExportable(dashboard) { - var dynSrv = new DynamicDashboardSrv(); - + makeExportable(dashboard: DashboardModel) { // clean up repeated rows and panels, // this is done on the live real dashboard instance, not on a clone // so we need to undo this // this is pretty hacky and needs to be changed - dynSrv.init(dashboard); - dynSrv.process({cleanUpOnly: true}); + dashboard.cleanUpRepeats(); var saveModel = dashboard.getSaveModelClone(); saveModel.id = null; // undo repeat cleanup - dynSrv.process(); + dashboard.processRepeats(); var inputs = []; var requires = {}; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 66a6b51abfea..90583d6be718 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -1,4 +1,5 @@ import {Emitter} from 'app/core/core'; +import _ from 'lodash'; export interface GridPos { x: number; @@ -21,6 +22,9 @@ export class PanelModel { alert?: any; scopedVars?: any; repeat?: any; + repeatIteration?: any; + repeatPanelId?: any; + repeatDirection?: any; // non persisted fullscreen: boolean; @@ -34,6 +38,10 @@ export class PanelModel { for (var property in model) { this[property] = model[property]; } + + if (!this.gridPos) { + this.gridPos = {x: 0, y: 0, h: 3, w: 6}; + } } getSaveModel() { @@ -43,7 +51,7 @@ export class PanelModel { continue; } - model[property] = this[property]; + model[property] = _.cloneDeep(this[property]); } return model; diff --git a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts index 8088aaebb958..09d7410303e2 100644 --- a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts +++ b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts @@ -1,287 +1,287 @@ -import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; - -import '../dashboard_srv'; -import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; - -function dynamicDashScenario(desc, func) { - - describe.skip(desc, function() { - var ctx: any = {}; - - ctx.setup = function (setupFunc) { - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(angularMocks.module(function($provide) { - $provide.value('contextSrv', { - user: { timezone: 'utc'} - }); - })); - - beforeEach(angularMocks.inject(function(dashboardSrv) { - ctx.dashboardSrv = dashboardSrv; - - var model = { - rows: [], - templating: { list: [] } - }; - - setupFunc(model); - ctx.dash = ctx.dashboardSrv.create(model); - ctx.dynamicDashboardSrv = new DynamicDashboardSrv(); - ctx.dynamicDashboardSrv.init(ctx.dash); - ctx.dynamicDashboardSrv.process(); - ctx.rows = ctx.dash.rows; - })); - }; - - func(ctx); - }); -} - -dynamicDashScenario('given dashboard with panel repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - panels: [{id: 2, repeat: 'apps'}] - }); - dash.templating.list.push({ - name: 'apps', - current: { - text: 'se1, se2, se3', - value: ['se1', 'se2', 'se3'] - }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - {text: 'se3', value: 'se3', selected: true}, - {text: 'se4', value: 'se4', selected: false} - ] - }); - }); - - it('should repeat panel one time', function() { - expect(ctx.rows[0].panels.length).to.be(3); - }); - - it('should mark panel repeated', function() { - expect(ctx.rows[0].panels[0].repeat).to.be('apps'); - expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); - }); - - it('should set scopedVars on panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); - expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); - expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3'); - }); - - describe('After a second iteration', function() { - var repeatedPanelAfterIteration1; - - beforeEach(function() { - repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; - ctx.rows[0].panels[0].fill = 10; - ctx.dynamicDashboardSrv.process(); - }); - - it('should have reused same panel instances', function() { - expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); - }); - - it('reused panel should copy properties from source', function() { - expect(ctx.rows[0].panels[1].fill).to.be(10); - }); - - it('should have same panel count', function() { - expect(ctx.rows[0].panels.length).to.be(3); - }); - }); - - describe('After a second iteration with different variable', function() { - beforeEach(function() { - ctx.dash.templating.list.push({ - name: 'server', - current: { text: 'se1, se2, se3', value: ['se1']}, - options: [{text: 'se1', value: 'se1', selected: true}] - }); - ctx.rows[0].panels[0].repeat = "server"; - ctx.dynamicDashboardSrv.process(); - }); - - it('should remove scopedVars value for last variable', function() { - expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined); - }); - - it('should have new variable value in scopedVars', function() { - expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1"); - }); - }); - - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { - ctx.dash.templating.list[0].options[1].selected = false; - ctx.dynamicDashboardSrv.process(); - }); - - it('should clean up repeated panel', function() { - expect(ctx.rows[0].panels.length).to.be(2); - }); - }); - - describe('After a second iteration and panel repeat is turned off', function() { - beforeEach(function() { - ctx.rows[0].panels[0].repeat = null; - ctx.dynamicDashboardSrv.process(); - }); - - it('should clean up repeated panel', function() { - expect(ctx.rows[0].panels.length).to.be(1); - }); - - it('should remove scoped vars from reused panel', function() { - expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined); - }); - }); - -}); - -dynamicDashScenario('given dashboard with row repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - repeat: 'servers', - panels: [{id: 2}] - }); - dash.rows.push({panels: []}); - dash.templating.list.push({ - name: 'servers', - current: { - text: 'se1, se2', - value: ['se1', 'se2'] - }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - ] - }); - }); - - it('should repeat row one time', function() { - expect(ctx.rows.length).to.be(3); - }); - - it('should keep panel ids on first row', function() { - expect(ctx.rows[0].panels[0].id).to.be(2); - }); - - it('should keep first row as repeat', function() { - expect(ctx.rows[0].repeat).to.be('servers'); - }); - - it('should clear repeat field on repeated row', function() { - expect(ctx.rows[1].repeat).to.be(null); - }); - - it('should add scopedVars to rows', function() { - expect(ctx.rows[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].scopedVars.servers.value).to.be('se2'); - }); - - it('should generate a repeartRowId based on repeat row index', function() { - expect(ctx.rows[1].repeatRowId).to.be(1); - expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration); - }); - - it('should set scopedVars on row panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); - }); - - describe('After a second iteration', function() { - var repeatedRowAfterFirstIteration; - - beforeEach(function() { - repeatedRowAfterFirstIteration = ctx.rows[1]; - ctx.rows[0].height = 500; - ctx.dynamicDashboardSrv.process(); - }); - - it('should still only have 2 rows', function() { - expect(ctx.rows.length).to.be(3); - }); - - it.skip('should have updated props from source', function() { - expect(ctx.rows[1].height).to.be(500); - }); - - it('should reuse row instance', function() { - expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); - }); - }); - - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { - ctx.dash.templating.list[0].options[1].selected = false; - ctx.dynamicDashboardSrv.process(); - }); - - it('should remove repeated second row', function() { - expect(ctx.rows.length).to.be(2); - }); - }); -}); - -dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - repeat: 'servers', - panels: [{id: 2, repeat: 'metric'}] - }); - dash.templating.list.push({ - name: 'servers', - current: { text: 'se1, se2', value: ['se1', 'se2'] }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - ] - }); - dash.templating.list.push({ - name: 'metric', - current: { text: 'm1, m2', value: ['m1', 'm2'] }, - options: [ - {text: 'm1', value: 'm1', selected: true}, - {text: 'm2', value: 'm2', selected: true}, - ] - }); - }); - - it('should repeat row one time', function() { - expect(ctx.rows.length).to.be(2); - }); - - it('should repeat panel on both rows', function() { - expect(ctx.rows[0].panels.length).to.be(2); - expect(ctx.rows[1].panels.length).to.be(2); - }); - - it('should keep panel ids on first row', function() { - expect(ctx.rows[0].panels[0].id).to.be(2); - }); - - it('should mark second row as repeated', function() { - expect(ctx.rows[0].repeat).to.be('servers'); - }); - - it('should clear repeat field on repeated row', function() { - expect(ctx.rows[1].repeat).to.be(null); - }); - - it('should generate a repeartRowId based on repeat row index', function() { - expect(ctx.rows[1].repeatRowId).to.be(1); - }); - - it('should set scopedVars on row panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); - }); - -}); +// import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; +// +// import '../dashboard_srv'; +// import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; +// +// function dynamicDashScenario(desc, func) { +// +// describe.skip(desc, function() { +// var ctx: any = {}; +// +// ctx.setup = function (setupFunc) { +// +// beforeEach(angularMocks.module('grafana.core')); +// beforeEach(angularMocks.module('grafana.services')); +// beforeEach(angularMocks.module(function($provide) { +// $provide.value('contextSrv', { +// user: { timezone: 'utc'} +// }); +// })); +// +// beforeEach(angularMocks.inject(function(dashboardSrv) { +// ctx.dashboardSrv = dashboardSrv; +// +// var model = { +// rows: [], +// templating: { list: [] } +// }; +// +// setupFunc(model); +// ctx.dash = ctx.dashboardSrv.create(model); +// ctx.dynamicDashboardSrv = new DynamicDashboardSrv(); +// ctx.dynamicDashboardSrv.init(ctx.dash); +// ctx.dynamicDashboardSrv.process(); +// ctx.rows = ctx.dash.rows; +// })); +// }; +// +// func(ctx); +// }); +// } +// +// dynamicDashScenario('given dashboard with panel repeat', function(ctx) { +// ctx.setup(function(dash) { +// dash.rows.push({ +// panels: [{id: 2, repeat: 'apps'}] +// }); +// dash.templating.list.push({ +// name: 'apps', +// current: { +// text: 'se1, se2, se3', +// value: ['se1', 'se2', 'se3'] +// }, +// options: [ +// {text: 'se1', value: 'se1', selected: true}, +// {text: 'se2', value: 'se2', selected: true}, +// {text: 'se3', value: 'se3', selected: true}, +// {text: 'se4', value: 'se4', selected: false} +// ] +// }); +// }); +// +// it('should repeat panel one time', function() { +// expect(ctx.rows[0].panels.length).to.be(3); +// }); +// +// it('should mark panel repeated', function() { +// expect(ctx.rows[0].panels[0].repeat).to.be('apps'); +// expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); +// }); +// +// it('should set scopedVars on panels', function() { +// expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); +// expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); +// expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3'); +// }); +// +// describe('After a second iteration', function() { +// var repeatedPanelAfterIteration1; +// +// beforeEach(function() { +// repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; +// ctx.rows[0].panels[0].fill = 10; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should have reused same panel instances', function() { +// expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); +// }); +// +// it('reused panel should copy properties from source', function() { +// expect(ctx.rows[0].panels[1].fill).to.be(10); +// }); +// +// it('should have same panel count', function() { +// expect(ctx.rows[0].panels.length).to.be(3); +// }); +// }); +// +// describe('After a second iteration with different variable', function() { +// beforeEach(function() { +// ctx.dash.templating.list.push({ +// name: 'server', +// current: { text: 'se1, se2, se3', value: ['se1']}, +// options: [{text: 'se1', value: 'se1', selected: true}] +// }); +// ctx.rows[0].panels[0].repeat = "server"; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should remove scopedVars value for last variable', function() { +// expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined); +// }); +// +// it('should have new variable value in scopedVars', function() { +// expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1"); +// }); +// }); +// +// describe('After a second iteration and selected values reduced', function() { +// beforeEach(function() { +// ctx.dash.templating.list[0].options[1].selected = false; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should clean up repeated panel', function() { +// expect(ctx.rows[0].panels.length).to.be(2); +// }); +// }); +// +// describe('After a second iteration and panel repeat is turned off', function() { +// beforeEach(function() { +// ctx.rows[0].panels[0].repeat = null; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should clean up repeated panel', function() { +// expect(ctx.rows[0].panels.length).to.be(1); +// }); +// +// it('should remove scoped vars from reused panel', function() { +// expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined); +// }); +// }); +// +// }); +// +// dynamicDashScenario('given dashboard with row repeat', function(ctx) { +// ctx.setup(function(dash) { +// dash.rows.push({ +// repeat: 'servers', +// panels: [{id: 2}] +// }); +// dash.rows.push({panels: []}); +// dash.templating.list.push({ +// name: 'servers', +// current: { +// text: 'se1, se2', +// value: ['se1', 'se2'] +// }, +// options: [ +// {text: 'se1', value: 'se1', selected: true}, +// {text: 'se2', value: 'se2', selected: true}, +// ] +// }); +// }); +// +// it('should repeat row one time', function() { +// expect(ctx.rows.length).to.be(3); +// }); +// +// it('should keep panel ids on first row', function() { +// expect(ctx.rows[0].panels[0].id).to.be(2); +// }); +// +// it('should keep first row as repeat', function() { +// expect(ctx.rows[0].repeat).to.be('servers'); +// }); +// +// it('should clear repeat field on repeated row', function() { +// expect(ctx.rows[1].repeat).to.be(null); +// }); +// +// it('should add scopedVars to rows', function() { +// expect(ctx.rows[0].scopedVars.servers.value).to.be('se1'); +// expect(ctx.rows[1].scopedVars.servers.value).to.be('se2'); +// }); +// +// it('should generate a repeartRowId based on repeat row index', function() { +// expect(ctx.rows[1].repeatRowId).to.be(1); +// expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration); +// }); +// +// it('should set scopedVars on row panels', function() { +// expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); +// expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); +// }); +// +// describe('After a second iteration', function() { +// var repeatedRowAfterFirstIteration; +// +// beforeEach(function() { +// repeatedRowAfterFirstIteration = ctx.rows[1]; +// ctx.rows[0].height = 500; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should still only have 2 rows', function() { +// expect(ctx.rows.length).to.be(3); +// }); +// +// it.skip('should have updated props from source', function() { +// expect(ctx.rows[1].height).to.be(500); +// }); +// +// it('should reuse row instance', function() { +// expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); +// }); +// }); +// +// describe('After a second iteration and selected values reduced', function() { +// beforeEach(function() { +// ctx.dash.templating.list[0].options[1].selected = false; +// ctx.dynamicDashboardSrv.process(); +// }); +// +// it('should remove repeated second row', function() { +// expect(ctx.rows.length).to.be(2); +// }); +// }); +// }); +// +// dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) { +// ctx.setup(function(dash) { +// dash.rows.push({ +// repeat: 'servers', +// panels: [{id: 2, repeat: 'metric'}] +// }); +// dash.templating.list.push({ +// name: 'servers', +// current: { text: 'se1, se2', value: ['se1', 'se2'] }, +// options: [ +// {text: 'se1', value: 'se1', selected: true}, +// {text: 'se2', value: 'se2', selected: true}, +// ] +// }); +// dash.templating.list.push({ +// name: 'metric', +// current: { text: 'm1, m2', value: ['m1', 'm2'] }, +// options: [ +// {text: 'm1', value: 'm1', selected: true}, +// {text: 'm2', value: 'm2', selected: true}, +// ] +// }); +// }); +// +// it('should repeat row one time', function() { +// expect(ctx.rows.length).to.be(2); +// }); +// +// it('should repeat panel on both rows', function() { +// expect(ctx.rows[0].panels.length).to.be(2); +// expect(ctx.rows[1].panels.length).to.be(2); +// }); +// +// it('should keep panel ids on first row', function() { +// expect(ctx.rows[0].panels[0].id).to.be(2); +// }); +// +// it('should mark second row as repeated', function() { +// expect(ctx.rows[0].repeat).to.be('servers'); +// }); +// +// it('should clear repeat field on repeated row', function() { +// expect(ctx.rows[1].repeat).to.be(null); +// }); +// +// it('should generate a repeartRowId based on repeat row index', function() { +// expect(ctx.rows[1].repeatRowId).to.be(1); +// }); +// +// it('should set scopedVars on row panels', function() { +// expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); +// expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); +// }); +// +// }); diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 65fa283b018f..47fe64f387f7 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -10,7 +10,6 @@ describe('given dashboard with repeated panels', function() { beforeEach(done => { dash = { - rows: [], templating: { list: [] }, annotations: { list: [] }, }; @@ -47,26 +46,6 @@ describe('given dashboard with repeated panels', function() { datasource: 'gfdb', }); - dash.rows.push({ - repeat: 'test', - panels: [ - {id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'}, - {id: 3, repeat: null, repeatPanelId: 2}, - { - id: 4, - datasource: '-- Mixed --', - targets: [{datasource: 'other'}], - }, - {id: 5, datasource: '$ds'}, - ] - }); - - dash.rows.push({ - repeat: null, - repeatRowId: 1, - panels: [], - }); - dash.panels = [ {id: 6, datasource: 'gfdb', type: 'graph'}, {id: 7}, @@ -78,6 +57,9 @@ describe('given dashboard with repeated panels', function() { {id: 9, datasource: '$ds'}, ]; + dash.panels.push({id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'}); + dash.panels.push({id: 3, repeat: null, repeatPanelId: 2}); + var datasourceSrvStub = {get: sinon.stub()}; datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({ name: 'gfdb', @@ -110,14 +92,6 @@ describe('given dashboard with repeated panels', function() { }); }); - it.skip('exported dashboard should not contain repeated panels', function() { - expect(exported.rows[0].panels.length).to.be(3); - }); - - it.skip('exported dashboard should not contain repeated rows', function() { - expect(exported.rows.length).to.be(1); - }); - it('should replace datasource refs', function() { var panel = exported.panels[0]; expect(panel.datasource).to.be("${DS_GFDB}"); diff --git a/public/app/features/dashboard/unsaved_changes_modal.ts b/public/app/features/dashboard/unsaved_changes_modal.ts index ab3ece1b8a28..9ab16ee077d8 100644 --- a/public/app/features/dashboard/unsaved_changes_modal.ts +++ b/public/app/features/dashboard/unsaved_changes_modal.ts @@ -22,9 +22,9 @@ const template = `
- - + +
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 55957d47762c..0e8e5618b227 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -1,7 +1,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; -import {profiler} from 'app/core/profiler'; +import {appEvents, profiler} from 'app/core/core'; import Remarkable from 'remarkable'; import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/dashboard_model'; @@ -188,7 +188,30 @@ export class PanelCtrl { }); } - removePanel() { + removePanel(ask: boolean) { + // confirm deletion + if (ask !== false) { + var text2, confirmText; + + if (this.panel.alert) { + text2 = "Panel includes an alert rule, removing panel will also remove alert rule"; + confirmText = "YES"; + } + + appEvents.emit('confirm-modal', { + title: 'Remove Panel', + text: 'Are you sure you want to remove this panel?', + text2: text2, + icon: 'fa-trash', + confirmText: confirmText, + yesText: 'Remove', + onConfirm: () => { + this.removePanel(false); + } + }); + return; + } + this.dashboard.removePanel(this.panel); } diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index be53968854b4..785afc7f43d6 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -24,6 +24,12 @@ +
+ Direction + +