diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index d370aab9d6d..6b2a300551e 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -44,8 +44,9 @@ export function infoPopover() { } }); - scope.$on('$destroy', function() { + var unbind = scope.$on('$destroy', function() { drop.destroy(); + unbind(); }); }); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index d44cbf4dbfb..5d30bc6d061 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -42,6 +42,8 @@ import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; import colors from './utils/colors'; +import {assignModelProperties} from './utils/model_utils'; +import {contextSrv} from './services/context_srv'; export { @@ -62,4 +64,6 @@ export { queryPartEditorDirective, WizardFlow, colors, + assignModelProperties, + contextSrv, }; diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 4cdc19e7b20..2b3f8a90eb4 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -23,12 +23,17 @@ export class Emitter { this.emitter.on(name, handler); if (scope) { - scope.$on('$destroy', () => { + var unbind = scope.$on('$destroy', () => { this.emitter.off(name, handler); + unbind(); }); } } + removeAllListeners(evt?) { + this.emitter.removeAllListeners(evt); + } + off(name, handler) { this.emitter.off(name, handler); } diff --git a/public/app/core/utils/model_utils.ts b/public/app/core/utils/model_utils.ts new file mode 100644 index 00000000000..3040094b1e3 --- /dev/null +++ b/public/app/core/utils/model_utils.ts @@ -0,0 +1,10 @@ +export function assignModelProperties(target, source, defaults) { + for (var key in defaults) { + if (!defaults.hasOwnProperty(key)) { + continue; + } + + target[key] = source[key] === undefined ? defaults[key] : source[key]; + } +} + diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 61c4d658ed1..cd72f6c6922 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -52,11 +52,12 @@ export class AlertTabCtrl { var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this); this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler); - // set panel alert edit mode - this.$scope.$on("$destroy", () => { + // set panel alert edit mode + var unbind = this.$scope.$on("$destroy", () => { this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler); this.panelCtrl.editingThresholds = false; this.panelCtrl.render(); + unbind(); }); // build notification model diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index e273a8a3b69..016bb87df3a 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -102,12 +102,7 @@ export class DashboardCtrl { }; $scope.addRowDefault = function() { - $scope.dashboard.rows.push({ - title: 'New row', - panels: [], - height: '250px', - isNew: true, - }); + $scope.dashboard.addEmptyRow(); }; $scope.showJsonEditor = function(evt, options) { @@ -122,8 +117,9 @@ export class DashboardCtrl { $timeout.cancel(resizeEventTimeout); resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); }); - $scope.$on('$destroy', function() { + var unbind = $scope.$on('$destroy', function() { angular.element(window).unbind('resize'); + unbind(); }); }; diff --git a/public/app/features/dashboard/keybindings.js b/public/app/features/dashboard/keybindings.js index 864538e19c1..d889126c764 100644 --- a/public/app/features/dashboard/keybindings.js +++ b/public/app/features/dashboard/keybindings.js @@ -11,8 +11,9 @@ function(angular, $) { this.shortcuts = function(scope) { - scope.$on('$destroy', function() { + var unbindDestroy = scope.$on('$destroy', function() { keyboardManager.unbindAll(); + unbindDestroy(); }); var helpModalScope = null; @@ -28,7 +29,11 @@ function(angular, $) { keyboard: false }); - helpModalScope.$on('$destroy', function() { helpModalScope = null; }); + var unbindModalDestroy = helpModalScope.$on('$destroy', function() { + helpModalScope = null; + unbindModalDestroy(); + }); + $q.when(helpModal).then(function(modalEl) { modalEl.modal('show'); }); }, { inputDisabled: true }); diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index bc67af627f7..aa75800817f 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -6,8 +6,8 @@ import moment from 'moment'; import _ from 'lodash'; import $ from 'jquery'; -import {Emitter} from 'app/core/core'; -import {contextSrv} from 'app/core/services/context_srv'; +import {Emitter, contextSrv} from 'app/core/core'; +import {DashboardRow} from './row/row_model'; export class DashboardModel { id: any; @@ -19,7 +19,7 @@ export class DashboardModel { timezone: any; editable: any; sharedCrosshair: any; - rows: any; + rows: DashboardRow[]; time: any; timepicker: any; templating: any; @@ -51,7 +51,6 @@ export class DashboardModel { this.timezone = data.timezone || ''; this.editable = data.editable !== false; this.sharedCrosshair = data.sharedCrosshair || false; - this.rows = data.rows || []; this.time = data.time || { from: 'now-6h', to: 'now' }; this.timepicker = data.timepicker || {}; this.templating = this.ensureListExist(data.templating); @@ -63,10 +62,15 @@ export class DashboardModel { this.links = data.links || []; this.gnetId = data.gnetId || null; + this.rows = []; + if (data.rows) { + for (let row of data.rows) { + this.rows.push(new DashboardRow(row)); + } + } + this.updateSchema(data); this.initMeta(meta); - - this.editMode = this.meta.isNew; } private initMeta(meta) { @@ -84,6 +88,7 @@ export class DashboardModel { } this.meta = meta; + this.editMode = this.meta.isNew; } // cleans meta data and other non peristent state @@ -91,18 +96,27 @@ export class DashboardModel { // temp remove stuff var events = this.events; var meta = this.meta; + var rows = this.rows; delete this.events; delete this.meta; + // prepare save model + this.rows = _.map(this.rows, row => row.getSaveModel()); events.emit('prepare-save-model'); + var copy = $.extend(true, {}, this); // restore properties this.events = events; this.meta = meta; + this.rows = rows; return copy; } + addEmptyRow() { + this.rows.push(new DashboardRow({isNew: true})); + } + private ensureListExist(data) { if (!data) { data = {}; } if (!data.list) { data.list = []; } diff --git a/public/app/features/dashboard/row/row.ts b/public/app/features/dashboard/row/row.ts index 97fd238c68e..b996c0109c6 100644 --- a/public/app/features/dashboard/row/row.ts +++ b/public/app/features/dashboard/row/row.ts @@ -19,7 +19,7 @@ export class DashRowCtrl { constructor(private $scope, private $rootScope, private $timeout, private uiSegmentSrv, private $q) { this.row.title = this.row.title || 'Row title'; - if (this.dashboard.meta.isNew) { + if (this.row.isNew) { this.dropView = 1; delete this.row.isNew; } @@ -200,13 +200,19 @@ coreModule.directive('panelDropZone', function($timeout) { } if (indrag === true) { - return showPanel(dropZoneSpan, 'Drop Here'); + var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row); + if (dropZoneSpan > 1) { + return showPanel(dropZoneSpan, 'Drop Here'); + } } hidePanel(); } - scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState); + row.events.on('panel-added', updateState); + row.events.on('span-changed', updateState); + + //scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState); scope.$on("ANGULAR_DRAG_START", function() { indrag = true; @@ -220,6 +226,7 @@ coreModule.directive('panelDropZone', function($timeout) { }); scope.$on("ANGULAR_DRAG_END", function() { + console.log('drag end'); indrag = false; updateState(); }); diff --git a/public/app/features/dashboard/row/row_ctrl.ts b/public/app/features/dashboard/row/row_ctrl.ts new file mode 100644 index 00000000000..2f5b1446a17 --- /dev/null +++ b/public/app/features/dashboard/row/row_ctrl.ts @@ -0,0 +1,231 @@ +/// + +import _ from 'lodash'; +import $ from 'jquery'; +import angular from 'angular'; + +import config from 'app/core/config'; +import {coreModule} from 'app/core/core'; + +import './options'; +import './add_panel'; + +export class DashRowCtrl { + dashboard: any; + row: any; + dropView: number; + + /** @ngInject */ + constructor(private $scope, private $rootScope, private $timeout, private uiSegmentSrv, private $q) { + this.row.title = this.row.title || 'Row title'; + + if (this.dashboard.meta.isNew) { + this.dropView = 1; + delete this.row.isNew; + } + } + + onDrop(panelId, dropTarget) { + var info = this.dashboard.getPanelInfoById(panelId); + if (dropTarget) { + var dropInfo = this.dashboard.getPanelInfoById(dropTarget.id); + dropInfo.row.panels[dropInfo.index] = info.panel; + info.row.panels[info.index] = dropTarget; + var dragSpan = info.panel.span; + info.panel.span = dropTarget.span; + dropTarget.span = dragSpan; + } else { + info.row.panels.splice(info.index, 1); + info.panel.span = 12 - this.dashboard.rowSpan(this.row); + this.row.panels.push(info.panel); + } + + this.$rootScope.$broadcast('render'); + } + + setHeight(height) { + this.row.height = height; + this.$scope.$broadcast('render'); + } + + moveRow(direction) { + var rowsList = this.dashboard.rows; + var rowIndex = _.indexOf(rowsList, this.row); + var newIndex = rowIndex; + switch (direction) { + case 'up': { + newIndex = rowIndex - 1; + break; + } + case 'down': { + newIndex = rowIndex + 1; + break; + } + case 'top': { + newIndex = 0; + break; + } + case 'bottom': { + newIndex = rowsList.length - 1; + break; + } + default: { + newIndex = rowIndex; + } + } + if (newIndex >= 0 && newIndex <= (rowsList.length - 1)) { + _.move(rowsList, rowIndex, newIndex); + } + } + + toggleCollapse() { + this.dropView = 0; + this.row.collapse = !this.row.collapse; + } + + showAddPanel() { + this.row.collapse = false; + this.dropView = this.dropView === 1 ? 0 : 1; + } + + showRowOptions() { + this.dropView = this.dropView === 2 ? 0 : 2; + } +} + +export function rowDirective($rootScope) { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/row/row.html', + controller: DashRowCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + dashboard: "=", + row: "=", + }, + link: function(scope, element) { + scope.$watchGroup(['ctrl.row.collapse', 'ctrl.row.height'], function() { + element.find('.panels-wrapper').css({minHeight: scope.ctrl.row.collapse ? '5px' : scope.ctrl.row.height}); + }); + + $rootScope.onAppEvent('panel-fullscreen-enter', function(evt, info) { + var hasPanel = _.find(scope.ctrl.row.panels, {id: info.panelId}); + if (!hasPanel) { + element.hide(); + } + }, scope); + + $rootScope.onAppEvent('panel-fullscreen-exit', function() { + element.show(); + }, scope); + } + }; +} + +coreModule.directive('dashRow', rowDirective); + + +coreModule.directive('panelWidth', function($rootScope) { + return function(scope, element) { + var fullscreen = false; + + function updateWidth() { + if (!fullscreen) { + element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%'; + } + } + + $rootScope.onAppEvent('panel-fullscreen-enter', function(evt, info) { + fullscreen = true; + + if (scope.panel.id !== info.panelId) { + element.hide(); + } else { + element[0].style.width = '100%'; + } + }, scope); + + $rootScope.onAppEvent('panel-fullscreen-exit', function(evt, info) { + fullscreen = false; + + if (scope.panel.id !== info.panelId) { + element.show(); + } + + updateWidth(); + }, scope); + + scope.$watch('panel.span', updateWidth); + + if (fullscreen) { + element.hide(); + } + }; +}); + + +coreModule.directive('panelDropZone', function($timeout) { + return function(scope, element) { + var row = scope.ctrl.row; + var indrag = false; + var textEl = element.find('.panel-drop-zone-text'); + + function showPanel(span, text) { + element.find('.panel-container').css('height', row.height); + element[0].style.width = ((span / 1.2) * 10) + '%'; + textEl.text(text); + element.show(); + } + + function hidePanel() { + element.hide(); + // element.removeClass('panel-drop-zone--empty'); + } + + function updateState() { + if (scope.ctrl.dashboard.editMode) { + if (row.panels.length === 0 && indrag === false) { + return showPanel(12, 'Empty Space'); + } + + var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row); + if (dropZoneSpan > 1) { + if (indrag) { + return showPanel(dropZoneSpan, 'Drop Here'); + } else { + return showPanel(dropZoneSpan, 'Empty Space'); + } + } + } + + if (indrag === true) { + return showPanel(dropZoneSpan, 'Drop Here'); + } + + hidePanel(); + } + + scope.row.events.on('panel-added', updateState); + scope.row.events.on('span-changed', updateState); + + scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState); + + scope.$on("ANGULAR_DRAG_START", function() { + indrag = true; + updateState(); + // $timeout(function() { + // var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row); + // if (dropZoneSpan > 0) { + // showPanel(dropZoneSpan, 'Panel Drop Zone'); + // } + // }); + }); + + scope.$on("ANGULAR_DRAG_END", function() { + indrag = false; + updateState(); + }); + }; +}); + diff --git a/public/app/features/dashboard/row/row_model.ts b/public/app/features/dashboard/row/row_model.ts new file mode 100644 index 00000000000..a3e0a99dba3 --- /dev/null +++ b/public/app/features/dashboard/row/row_model.ts @@ -0,0 +1,32 @@ +/// + +import {Emitter, contextSrv} from 'app/core/core'; +import {assignModelProperties} from 'app/core/core'; + +export class DashboardRow { + panels: any; + title: any; + showTitle: any; + titleSize: any; + events: Emitter; + + defaults = { + title: 'Dashboard Row', + panels: [], + showTitle: false, + titleSize: 'h6', + height: 250, + isNew: false, + }; + + constructor(private model) { + assignModelProperties(this, model, this.defaults); + this.events = new Emitter(); + } + + getSaveModel() { + assignModelProperties(this.model, this, this.defaults); + return this.model; + } +} + diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index e1132492d4c..60b8f24c462 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -65,6 +65,7 @@ function(angular, _) { dash.time = 0; dash.refresh = 0; dash.schemaVersion = 0; + dash.editMode = false; // filter row and panels properties that should be ignored dash.rows = _.filter(dash.rows, function(row) { diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 0645114ebf2..6a70de5ad63 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -199,8 +199,9 @@ function (angular, _, $) { } } - panelScope.$on('$destroy', function() { + var unbind = panelScope.$on('$destroy', function() { self.panelScopes = _.without(self.panelScopes, panelScope); + unbind(); }); }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 3bcca4f3f72..52104e5411d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -50,7 +50,12 @@ export class PanelCtrl { $scope.$on("refresh", () => this.refresh()); $scope.$on("render", () => this.render()); - $scope.$on("$destroy", () => this.events.emit('panel-teardown')); + + var unbindDestroy = $scope.$on("$destroy", () => { + this.events.emit('panel-teardown'); + this.events.removeAllListeners(); + unbindDestroy(); + }); } init() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 30785eadd69..44c012f772d 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -100,7 +100,6 @@ module.directive('grafanaPanel', function() { panelContainer.removeClass('panel-alert-state--' + lastAlertState); lastAlertState = null; } - }); scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() { @@ -189,8 +188,9 @@ module.directive('panelResizer', function($rootScope) { elem.on('mousedown', dragStartHandler); - scope.$on("$destroy", function() { + var unbind = scope.$on("$destroy", function() { elem.off('mousedown', dragStartHandler); + unbind(); }); } }; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 3e12b65ec16..0cd0cb9f847 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; +import {assignModelProperties} from 'app/core/core'; export interface Variable { setValue(option); @@ -13,12 +14,9 @@ export interface Variable { } export var variableTypes = {}; - -export function assignModelProperties(target, source, defaults) { - _.forEach(defaults, function(value, key) { - target[key] = source[key] === undefined ? value : source[key]; - }); -} +export { + assignModelProperties +}; export function containsVariable(...args: any[]) { var variableName = args[args.length-1]; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 9d7349e7dbc..ed8cd9462aa 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -34,6 +34,16 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var rootScope = scope.$root; var panelWidth = 0; var thresholdManager = new ThresholdManager(ctrl); + var plot; + + ctrl.events.on('panel-teardown', () => { + thresholdManager = null; + + if (plot) { + plot.destroy(); + plot = null; + } + }); rootScope.onAppEvent('setCrosshair', function(event, info) { // do not need to to this if event is from this panel @@ -42,7 +52,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { } if (dashboard.sharedCrosshair) { - var plot = elem.data().plot; if (plot) { plot.setCrosshair({ x: info.pos.x, y: info.pos.y }); } @@ -50,10 +59,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { }, scope); rootScope.onAppEvent('clearCrosshair', function() { - var plot = elem.data().plot; - if (plot) { - plot.clearCrosshair(); - } + plot.clearCrosshair(); }, scope); // Receive render events @@ -287,7 +293,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { function callPlot(incrementRenderCounter) { try { - $.plot(elem, sortedSeries, options); + plot = $.plot(elem, sortedSeries, options); if (ctrl.renderError) { delete ctrl.error; delete ctrl.inspector; @@ -529,9 +535,9 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { return "%H:%M"; } - new GraphTooltip(elem, dashboard, scope, function() { - return sortedSeries; - }); + // new GraphTooltip(elem, dashboard, scope, function() { + // return sortedSeries; + // }); elem.bind("plotselected", function (event, ranges) { scope.$apply(function() { diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts index 069f5f38292..5bb0a8b561a 100644 --- a/public/app/plugins/panel/graph/threshold_manager.ts +++ b/public/app/plugins/panel/graph/threshold_manager.ts @@ -153,8 +153,8 @@ export class ThresholdManager { this.renderHandle(1, this.height-30); } - this.placeholder.off('mousedown', '.alert-handle'); - this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this)); + // this.placeholder.off('mousedown', '.alert-handle'); + // this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this)); this.needsCleanup = true; } diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index f323bf4290e..1eac74b2539 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -17,9 +17,10 @@ export class ThresholdFormCtrl { this.disabled = true; } - $scope.$on("$destroy", () => { + var unbindDestroy = $scope.$on("$destroy", () => { this.panelCtrl.editingThresholds = false; this.panelCtrl.render(); + unbindDestroy(); }); this.panelCtrl.editingThresholds = true; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index d8d174c5c84..35448d6a296 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -212,8 +212,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { elem.on('click', '.table-panel-page-link', switchPage); - scope.$on('$destroy', function() { + var unbindDestroy = scope.$on('$destroy', function() { elem.off('click', '.table-panel-page-link'); + unbindDestroy(); }); ctrl.events.on('render', function(renderData) {