Explorar o código

Merge branch 'panel_id_permalink'

Torkel Ödegaard %!s(int64=11) %!d(string=hai) anos
pai
achega
2b865e3505

+ 5 - 3
src/app/controllers/dash.js

@@ -11,7 +11,9 @@ function (angular, $, config, _) {
   var module = angular.module('grafana.controllers');
 
   module.controller('DashCtrl', function(
-    $scope, $rootScope, dashboardKeybindings, filterSrv, dashboardSrv, panelMoveSrv, timer) {
+      $scope, $rootScope, dashboardKeybindings,
+      filterSrv, dashboardSrv, dashboardViewStateSrv,
+      panelMoveSrv, timer) {
 
     $scope.editor = { index: 0 };
     $scope.panelNames = config.panels;
@@ -24,9 +26,9 @@ function (angular, $, config, _) {
     $scope.setupDashboard = function(event, dashboardData) {
       timer.cancel_all();
 
-      $rootScope.fullscreen = false;
-
       $scope.dashboard = dashboardSrv.create(dashboardData);
+      $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
+
       $scope.grafana.style = $scope.dashboard.style;
 
       $scope.filter = filterSrv;

+ 2 - 4
src/app/controllers/dashLoader.js

@@ -14,6 +14,7 @@ function (angular, _, moment, config) {
 
     $scope.init = function() {
       $scope.db = datasourceSrv.getGrafanaDB();
+
       $scope.onAppEvent('save-dashboard', function() {
         $scope.saveDashboard();
       });
@@ -23,10 +24,6 @@ function (angular, _, moment, config) {
       });
     };
 
-    $scope.exitFullscreen = function() {
-      $scope.emitAppEvent('panel-fullscreen-exit');
-    };
-
     $scope.set_default = function() {
       window.localStorage.grafanaDashboardDefault = $location.path();
       alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
@@ -78,6 +75,7 @@ function (angular, _, moment, config) {
         .then(function(result) {
           alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
 
+          $location.search({});
           $location.path(result.url);
 
           $rootScope.$emit('dashboard-saved', $scope.dashboard);

+ 5 - 60
src/app/controllers/row.js

@@ -32,36 +32,13 @@ function (angular, app, _) {
       }
     };
 
-    $scope.rowSpan = function(row) {
-      return _.reduce(row.panels, function(p,v) {
-        return p + v.span;
-      },0);
-    };
-
-    // This can be overridden by individual panels
+     // This can be overridden by individual panels
     $scope.close_edit = function() {
       $scope.$broadcast('render');
     };
 
     $scope.add_panel = function(panel) {
-      var rowSpan = $scope.rowSpan($scope.row);
-      var panelCount = $scope.row.panels.length;
-      var space = (12 - rowSpan) - panel.span;
-
-      // try to make room of there is no space left
-      if (space <= 0) {
-        if (panelCount === 1) {
-          $scope.row.panels[0].span = 6;
-          panel.span = 6;
-        }
-        else if (panelCount === 2) {
-          $scope.row.panels[0].span = 4;
-          $scope.row.panels[1].span = 4;
-          panel.span = 4;
-        }
-      }
-
-      $scope.row.panels.push(panel);
+      $scope.dashboard.add_panel(panel, $scope.row);
     };
 
     $scope.delete_row = function() {
@@ -100,45 +77,17 @@ function (angular, app, _) {
     };
 
     $scope.duplicatePanel = function(panel, row) {
-      row = row || $scope.row;
-      var currentRowSpan = $scope.rowSpan(row);
-      if (currentRowSpan <= 9) {
-        row.panels.push(angular.copy(panel));
-      }
-      else {
-        var rowsList = $scope.dashboard.rows;
-        var rowIndex = _.indexOf(rowsList, row);
-        if (rowIndex === rowsList.length - 1) {
-          var newRow = angular.copy($scope.row);
-          newRow.panels = [];
-          $scope.dashboard.rows.push(newRow);
-          $scope.duplicatePanel(panel, newRow);
-        }
-        else {
-          $scope.duplicatePanel(panel, rowsList[rowIndex+1]);
-        }
-      }
+      $scope.dashboard.duplicatePanel(panel, row || $scope.row);
     };
 
     $scope.reset_panel = function(type) {
-      var
-        defaultSpan = 12,
-        _as = 12-$scope.rowSpan($scope.row);
+      var defaultSpan = 12;
+      var _as = 12 - $scope.dashboard.rowSpan($scope.row);
 
       $scope.panel = {
         error   : false,
-        /** @scratch /panels/1
-         * span:: A number, 1-12, that describes the width of the panel.
-         */
         span    : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
-        /** @scratch /panels/1
-         * editable:: Enable or disable the edit button the the panel
-         */
         editable: true,
-        /** @scratch /panels/1
-         * type:: The type of panel this object contains. Each panel type will require additional
-         * properties. See the panel types list to the right.
-         */
         type    : type
       };
 
@@ -155,10 +104,6 @@ function (angular, app, _) {
       $scope.row.height = fixRowHeight($scope.row.height);
     };
 
-    /** @scratch /panels/2
-     * --
-     */
-
     $scope.init();
 
   });

+ 1 - 0
src/app/controllers/search.js

@@ -41,6 +41,7 @@ function (angular, _, config, $) {
 
         var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
         if (selectedDash) {
+          $location.search({});
           $location.path("/dashboard/db/" + selectedDash.id);
           setTimeout(function() {
             $('body').click(); // hack to force dropdown to close;

+ 4 - 1
src/app/directives/grafanaGraph.js

@@ -21,7 +21,6 @@ function (angular, $, kbn, moment, _) {
         var legendSideLastValue = null;
 
         scope.$on('refresh',function() {
-          if (scope.otherPanelInFullscreenMode()) { return; }
           scope.get_data();
         });
 
@@ -39,6 +38,10 @@ function (angular, $, kbn, moment, _) {
         // Receive render events
         scope.$on('render',function(event, renderData) {
           data = renderData || data;
+          if (!data) {
+            scope.get_data();
+            return;
+          }
           annotations = data.annotations || annotations;
           render_panel();
         });

+ 2 - 16
src/app/panels/graph/module.js

@@ -188,13 +188,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
 
-    $scope.init = function() {
-      panelSrv.init($scope);
-      $scope.hiddenSeries = {};
-      if (!$scope.skipDataOnInit) {
-        $scope.get_data();
-      }
-    };
+    $scope.hiddenSeries = {};
 
     $scope.updateTimeRange = function () {
       $scope.range = $scope.filter.timeRange();
@@ -210,10 +204,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     };
 
     $scope.get_data = function() {
-      delete $scope.panel.error;
-
-      $scope.panelMeta.loading = true;
-
       $scope.updateTimeRange();
 
       var metricsQuery = {
@@ -297,10 +287,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       return series;
     };
 
-    $scope.otherPanelInFullscreenMode = function() {
-      return $rootScope.fullscreen && !$scope.fullscreen;
-    };
-
     $scope.render = function(data) {
       $scope.$emit('render', data);
     };
@@ -371,7 +357,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       $scope.render();
     };
 
-    $scope.init();
+    panelSrv.init($scope);
   });
 
 });

+ 1 - 1
src/app/partials/dashLoader.html

@@ -4,7 +4,7 @@
   }
 </style>
 
-<li ng-show="fullscreen">
+<li ng-show="dashboardViewState.fullscreen">
   <a ng-click="exitFullscreen()">
     Back to dashboard
   </a>

+ 2 - 2
src/app/partials/dashboard.html

@@ -1,4 +1,4 @@
-<div ng-controller="DashCtrl" body-class>
+<div ng-controller="DashCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
 
   <div class="navbar navbar-static-top">
     <div class="navbar-inner">
@@ -105,7 +105,7 @@
                 </div>
               </div>
 
-              <div ng-show="rowSpan(row) < 10 && dashboard.$$panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
+              <div ng-show="dashboard.rowSpan(row) < 10 && dashboard.$$panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
               </div>
 
               <div class="clearfix"></div>

+ 2 - 2
src/app/partials/roweditor.html

@@ -27,7 +27,7 @@
         <thead>
           <th>Title</th>
           <th>Type</th>
-          <th>Span <span class="small">({{rowSpan(row)}}/12)</span></th>
+          <th>Span <span class="small">({{dashboard.rowSpan(row)}}/12)</span></th>
           <th>Delete</th>
           <th>Move</th>
           <th></th>
@@ -49,7 +49,7 @@
     <h4>Select Panel Type</h4>
     <form class="form-inline">
       <select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in availablePanels|stringSort"></select>
-      <small ng-show="rowSpan(row) > 11">
+      <small ng-show="dashboard.rowSpan(row) > 11">
         Note: This row is full, new panels will wrap to a new line. You should add another row.
       </small>
     </form>

+ 1 - 0
src/app/routes/dashboard-from-db.js

@@ -11,6 +11,7 @@ function (angular) {
       .when('/dashboard/db/:id', {
         templateUrl: 'app/partials/dashboard.html',
         controller : 'DashFromDBProvider',
+        reloadOnSearch: false,
       })
       .when('/dashboard/elasticsearch/:id', {
         templateUrl: 'app/partials/dashboard.html',

+ 1 - 0
src/app/services/all.js

@@ -11,5 +11,6 @@ define([
   './unsavedChangesSrv',
   './dashboard/dashboardKeyBindings',
   './dashboard/dashboardSrv',
+  './dashboard/dashboardViewStateSrv',
 ],
 function () {});

+ 1 - 15
src/app/services/dashboard/dashboardKeyBindings.js

@@ -12,20 +12,6 @@ function(angular, $) {
 
     this.shortcuts = function(scope) {
 
-      scope.onAppEvent('panel-fullscreen-enter', function() {
-        $rootScope.fullscreen = true;
-      });
-
-      scope.onAppEvent('panel-fullscreen-exit', function() {
-        $rootScope.fullscreen = false;
-      });
-
-      scope.onAppEvent('dashboard-saved', function() {
-        if ($rootScope.fullscreen) {
-          scope.emitAppEvent('panel-fullscreen-exit');
-        }
-      });
-
       scope.$on('$destroy', function() {
         keyboardManager.unbind('ctrl+f');
         keyboardManager.unbind('ctrl+h');
@@ -67,7 +53,7 @@ function(angular, $) {
           modalData.$scope.dismiss();
         }
 
-        scope.emitAppEvent('panel-fullscreen-exit');
+        scope.exitFullscreen();
       }, { inputDisabled: true });
     };
   });

+ 85 - 5
src/app/services/dashboard/dashboardSrv.js

@@ -10,7 +10,7 @@ function (angular, $, kbn, _) {
 
   var module = angular.module('grafana.services');
 
-  module.service('dashboardSrv', function(timer, $rootScope, $timeout) {
+  module.factory('dashboardSrv', function(timer, $rootScope, $timeout) {
 
     function DashboardModel (data) {
 
@@ -29,6 +29,8 @@ function (angular, $, kbn, _) {
       this.time = data.time || { from: 'now-6h', to: 'now' };
       this.templating = data.templating || { list: [] };
       this.refresh = data.refresh;
+      this.version = data.version || 0;
+      this.$state = data.$state;
 
       if (this.nav.length === 0) {
         this.nav.push({ type: 'timepicker' });
@@ -47,6 +49,65 @@ function (angular, $, kbn, _) {
 
     var p = DashboardModel.prototype;
 
+    p.getNextPanelId = function() {
+      var i, j, row, panel, max = 0;
+      for (i = 0; i < this.rows.length; i++) {
+        row = this.rows[i];
+        for (j = 0; j < row.panels.length; j++) {
+          panel = row.panels[j];
+          if (panel.id > max) { max = panel.id; }
+        }
+      }
+      return max + 1;
+    };
+
+    p.rowSpan = function(row) {
+      return _.reduce(row.panels, function(p,v) {
+        return p + v.span;
+      },0);
+    };
+
+    p.add_panel = function(panel, row) {
+      var rowSpan = this.rowSpan(row);
+      var panelCount = row.panels.length;
+      var space = (12 - rowSpan) - panel.span;
+      panel.id = this.getNextPanelId();
+
+      // try to make room of there is no space left
+      if (space <= 0) {
+        if (panelCount === 1) {
+          row.panels[0].span = 6;
+          panel.span = 6;
+        }
+        else if (panelCount === 2) {
+          row.panels[0].span = 4;
+          row.panels[1].span = 4;
+          panel.span = 4;
+        }
+      }
+
+      row.panels.push(panel);
+    };
+
+    p.duplicatePanel = function(panel, row) {
+      var rowIndex = _.indexOf(this.rows, row);
+      var newPanel = angular.copy(panel);
+      newPanel.id = this.getNextPanelId();
+
+      while(rowIndex < this.rows.length) {
+        var currentRow = this.rows[rowIndex];
+        if (this.rowSpan(currentRow) <= 9) {
+          currentRow.panels.push(newPanel);
+          return;
+        }
+        rowIndex++;
+      }
+
+      var newRow = angular.copy(row);
+      newRow.panels = [newPanel];
+      this.rows.push(newRow);
+    };
+
     p.emit_refresh = function() {
       $rootScope.$broadcast('refresh');
     };
@@ -75,12 +136,32 @@ function (angular, $, kbn, _) {
 
     p.updateSchema = function(old) {
       var i, j, row, panel;
-      var isChanged = false;
+      var oldVersion = this.version;
+      this.version = 3;
+
+      if (oldVersion === 3) {
+        return;
+      }
+
+      // Version 3 schema changes
+      // ensure panel ids
+      var maxId = this.getNextPanelId();
+      for (i = 0; i < this.rows.length; i++) {
+        row = this.rows[i];
+        for (j = 0; j < row.panels.length; j++) {
+          panel = row.panels[j];
+          if (!panel.id) {
+            panel.id = maxId;
+            maxId += 1;
+          }
+        }
+      }
 
-      if (this.version === 2) {
+      if (oldVersion === 2) {
         return;
       }
 
+      // Version 2 schema changes
       if (old.services) {
         if (old.services.filter) {
           this.time = old.services.filter.time;
@@ -95,7 +176,6 @@ function (angular, $, kbn, _) {
           panel = row.panels[j];
           if (panel.type === 'graphite') {
             panel.type = 'graph';
-            isChanged = true;
           }
 
           if (panel.type === 'graph') {
@@ -128,7 +208,7 @@ function (angular, $, kbn, _) {
         }
       }
 
-      this.version = 2;
+      this.version = 3;
     };
 
     return {

+ 157 - 0
src/app/services/dashboard/dashboardViewStateSrv.js

@@ -0,0 +1,157 @@
+define([
+  'angular',
+  'lodash',
+  'jquery',
+],
+function (angular, _, $) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.factory('dashboardViewStateSrv', function($location, $timeout) {
+
+    // represents the transient view state
+    // like fullscreen panel & edit
+    function DashboardViewState($scope) {
+      var self = this;
+
+      $scope.exitFullscreen = function() {
+        self.update({ fullscreen: false });
+      };
+
+      $scope.onAppEvent('dashboard-saved', function() {
+        self.update({ fullscreen: false });
+      });
+
+      $scope.onAppEvent('$routeUpdate', function() {
+        var urlState = self.getQueryStringState();
+        console.log("route updated!");
+        if (self.needsSync(urlState)) {
+          self.update(urlState, true);
+        }
+      });
+
+      this.panelScopes = [];
+      this.$scope = $scope;
+
+      this.update(this.getQueryStringState(), true);
+    }
+
+    DashboardViewState.prototype.needsSync = function(urlState) {
+      if (urlState.fullscreen !== this.fullscreen) { return true; }
+      if (urlState.edit !== this.edit) { return true; }
+      if (urlState.panelId !== this.panelId) { return true; }
+      return false;
+    };
+
+    DashboardViewState.prototype.getQueryStringState = function() {
+      var queryParams = $location.search();
+      return {
+        panelId: parseInt(queryParams.panelId) || null,
+        fullscreen: queryParams.fullscreen ? true : false,
+        edit: queryParams.edit ? true : false
+      };
+    };
+
+    DashboardViewState.prototype.update = function(state, skipUrlSync) {
+      _.extend(this, state);
+
+      if (!this.fullscreen) {
+        this.panelId = null;
+        this.edit = false;
+      }
+
+      if (!skipUrlSync) {
+        $location.search({
+          fullscreen: this.fullscreen ? true : null,
+          panelId: this.panelId,
+          edit: this.edit ? true : null
+        });
+      }
+
+      this.syncState();
+    };
+
+    DashboardViewState.prototype.syncState = function() {
+      if (this.panelScopes.length === 0) { return; }
+
+      if (this.fullscreen) {
+        if (this.fullscreenPanel) {
+          this.leaveFullscreen(false);
+        }
+        var panelScope = this.getPanelScope(this.panelId);
+        this.enterFullscreen(panelScope);
+        return;
+      }
+
+      if (this.fullscreenPanel) {
+        this.leaveFullscreen(true);
+      }
+    };
+
+    DashboardViewState.prototype.getPanelScope = function(id) {
+      return _.find(this.panelScopes, function(panelScope) {
+        return panelScope.panel.id === id;
+      });
+    };
+
+    DashboardViewState.prototype.leaveFullscreen = function(render) {
+      var self = this;
+
+      self.fullscreenPanel.editMode = false;
+      self.fullscreenPanel.fullscreen = false;
+      delete self.fullscreenPanel.height;
+
+      if (!render) { return false;}
+
+      $timeout(function() {
+        if (self.oldTimeRange !== self.fullscreenPanel.range) {
+          self.$scope.dashboard.emit_refresh();
+        }
+        else {
+          self.fullscreenPanel.$emit('render');
+        }
+        delete self.fullscreenPanel;
+      });
+    };
+
+    DashboardViewState.prototype.enterFullscreen = function(panelScope) {
+      var docHeight = $(window).height();
+      var editHeight = Math.floor(docHeight * 0.3);
+      var fullscreenHeight = Math.floor(docHeight * 0.7);
+      this.oldTimeRange = panelScope.range;
+
+      panelScope.height = this.edit ? editHeight : fullscreenHeight;
+      panelScope.editMode = this.edit;
+      this.fullscreenPanel = panelScope;
+
+      $(window).scrollTop(0);
+
+      panelScope.fullscreen = true;
+
+      $timeout(function() {
+        panelScope.$emit('render');
+      });
+    };
+
+    DashboardViewState.prototype.registerPanel = function(panelScope) {
+      var self = this;
+      self.panelScopes.push(panelScope);
+
+      if (self.panelId === panelScope.panel.id) {
+        self.enterFullscreen(panelScope);
+      }
+
+      panelScope.$on('$destroy', function() {
+        self.panelScopes = _.without(self.panelScopes, panelScope);
+      });
+    };
+
+    return {
+      create: function($scope) {
+        return new DashboardViewState($scope);
+      }
+    };
+
+  });
+});

+ 25 - 58
src/app/services/panelSrv.js

@@ -1,9 +1,8 @@
 define([
   'angular',
   'lodash',
-  'jquery',
 ],
-function (angular, _, $) {
+function (angular, _) {
   'use strict';
 
   var module = angular.module('grafana.services');
@@ -22,12 +21,12 @@ function (angular, _, $) {
         },
         {
           text: 'Edit',
-          click: "toggleFullscreenEdit()",
+          click: "toggleFullscreen(true)",
           condition: $scope.panelMeta.fullscreenEdit
         },
         {
           text: "Fullscreen",
-          click: 'toggleFullscreen()',
+          click: 'toggleFullscreen(false)',
           condition: $scope.panelMeta.fullscreenView
         },
         {
@@ -71,46 +70,6 @@ function (angular, _, $) {
         });
       };
 
-      $scope.enterFullscreenMode = function(options) {
-        var docHeight = $(window).height();
-        var editHeight = Math.floor(docHeight * 0.3);
-        var fullscreenHeight = Math.floor(docHeight * 0.7);
-        var oldTimeRange = $scope.range;
-
-        $scope.height = options.edit ? editHeight : fullscreenHeight;
-        $scope.editMode = options.edit;
-
-        if (!$scope.fullscreen) {
-          var closeEditMode = $rootScope.$on('panel-fullscreen-exit', function() {
-            $scope.editMode = false;
-            $scope.fullscreen = false;
-            delete $scope.height;
-
-            closeEditMode();
-
-            $timeout(function() {
-              if (oldTimeRange !== $scope.range) {
-                $scope.dashboard.emit_refresh();
-              }
-              else {
-                $scope.$emit('render');
-              }
-            });
-          });
-        }
-
-        $(window).scrollTop(0);
-
-        $scope.fullscreen = true;
-
-        $rootScope.$emit('panel-fullscreen-enter');
-
-        $timeout(function() {
-          $scope.$emit('render');
-        });
-
-      };
-
       $scope.addDataQuery = function() {
         $scope.panel.targets.push({target: ''});
       };
@@ -135,22 +94,12 @@ function (angular, _, $) {
         $scope.get_data();
       };
 
-      $scope.toggleFullscreenEdit = function() {
-        if ($scope.editMode) {
-          $rootScope.$emit('panel-fullscreen-exit');
-          return;
-        }
-
-        $scope.enterFullscreenMode({edit: true});
+      $scope.toggleFullscreen = function(edit) {
+        $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
       };
 
-      $scope.toggleFullscreen = function() {
-        if ($scope.fullscreen && !$scope.editMode) {
-          $rootScope.$emit('panel-fullscreen-exit');
-          return;
-        }
-
-        $scope.enterFullscreenMode({ edit: false });
+      $scope.otherPanelInFullscreenMode = function() {
+        return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
       };
 
       // Post init phase
@@ -162,6 +111,24 @@ function (angular, _, $) {
 
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.setDatasource($scope.panel.datasource);
+
+      $scope.dashboardViewState.registerPanel($scope);
+
+      if ($scope.get_data) {
+        var panel_get_data = $scope.get_data;
+        $scope.get_data = function() {
+          if ($scope.otherPanelInFullscreenMode()) { return; }
+
+          delete $scope.panel.error;
+          $scope.panelMeta.loading = true;
+
+          panel_get_data();
+        };
+
+        if (!$scope.skipDataOnInit) {
+          $scope.get_data();
+        }
+      }
     };
   });
 

+ 1 - 0
src/app/services/playlistSrv.js

@@ -68,6 +68,7 @@ function (angular, _, kbn) {
       timerInstance = setInterval(function() {
         $rootScope.$apply(function() {
           angular.element(window).unbind('resize');
+          $location.search({});
           $location.path(dashboards[index % dashboards.length].url);
           index++;
         });

+ 7 - 0
src/app/services/unsavedChangesSrv.js

@@ -28,10 +28,12 @@ function(angular, _, config) {
     $rootScope.$on("dashboard-saved", function(event, savedDashboard) {
       self.original = angular.copy(savedDashboard);
       self.current = savedDashboard;
+      self.orignalPath = $location.path();
     });
 
     $rootScope.$on("$routeChangeSuccess", function() {
       self.original = null;
+      self.originalPath = $location.path();
     });
 
     window.onbeforeunload = function() {
@@ -42,6 +44,11 @@ function(angular, _, config) {
 
     this.init = function() {
       $rootScope.$on("$locationChangeStart", function(event, next) {
+        if (self.originalPath === $location.path()) {
+          console.log("skipping");
+          return;
+        }
+
         if (self.has_unsaved_changes()) {
           event.preventDefault();
           self.next = next;

+ 1 - 1
src/index.html

@@ -27,7 +27,7 @@
       <strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
     </div>
 
-    <div ng-view ng-class="{'dashboard-fullscreen': fullscreen}"></div>
+    <div ng-view ></div>
 
   </body>
 </html>

+ 65 - 4
src/test/specs/dashboardSrv-specs.js

@@ -7,7 +7,6 @@ define([
     var model;
 
     beforeEach(module('grafana.services'));
-
     beforeEach(inject(function(dashboardSrv) {
       model = dashboardSrv.create({});
     }));
@@ -24,12 +23,71 @@ define([
 
   });
 
+  describe('when getting next panel id', function() {
+    var model;
+
+    beforeEach(module('grafana.services'));
+    beforeEach(inject(function(dashboardSrv) {
+      model = dashboardSrv.create({
+        rows: [{ panels: [{ id: 5 }]}]
+      });
+    }));
+
+    it('should return max id + 1', function() {
+      expect(model.getNextPanelId()).to.be(6);
+    });
+  });
+
+  describe('row and panel manipulation', function() {
+    var dashboard;
+
+    beforeEach(module('grafana.services'));
+    beforeEach(inject(function(dashboardSrv) {
+      dashboard = dashboardSrv.create({});
+    }));
+
+    it('row span should sum spans', function() {
+      var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
+      expect(spanLeft).to.be(5);
+    });
+
+    it('adding default should split span in half', function() {
+      dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
+      dashboard.add_panel({span: 4}, dashboard.rows[0]);
+
+      expect(dashboard.rows[0].panels[0].span).to.be(6);
+      expect(dashboard.rows[0].panels[1].span).to.be(6);
+      expect(dashboard.rows[0].panels[1].id).to.be(8);
+    });
+
+    it('duplicate panel should try to add it to same row', function() {
+      var panel = { span: 4, attr: '123', id: 10 };
+      dashboard.rows = [{ panels: [panel] }];
+      dashboard.duplicatePanel(panel, dashboard.rows[0]);
+
+      expect(dashboard.rows[0].panels[0].span).to.be(4);
+      expect(dashboard.rows[0].panels[1].span).to.be(4);
+      expect(dashboard.rows[0].panels[1].attr).to.be('123');
+      expect(dashboard.rows[0].panels[1].id).to.be(11);
+    });
+
+    it('duplicate should add row if there is no space left', function() {
+      var panel = { span: 12, attr: '123' };
+      dashboard.rows = [{ panels: [panel] }];
+      dashboard.duplicatePanel(panel, dashboard.rows[0]);
+
+      expect(dashboard.rows[0].panels[0].span).to.be(12);
+      expect(dashboard.rows[0].panels.length).to.be(1);
+      expect(dashboard.rows[1].panels[0].attr).to.be('123');
+    });
+
+  });
+
   describe('when creating dashboard with old schema', function() {
     var model;
     var graph;
 
     beforeEach(module('grafana.services'));
-
     beforeEach(inject(function(dashboardSrv) {
       model = dashboardSrv.create({
         services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [1] }},
@@ -54,6 +112,10 @@ define([
       expect(model.title).to.be('No Title');
     });
 
+    it('should have panel id', function() {
+      expect(graph.id).to.be(1);
+    });
+
     it('should move time and filtering list', function() {
       expect(model.time.from).to.be('now-1d');
       expect(model.templating.list[0]).to.be(1);
@@ -73,10 +135,9 @@ define([
     });
 
     it('dashboard schema version should be set to latest', function() {
-      expect(model.version).to.be(2);
+      expect(model.version).to.be(3);
     });
 
   });
 
-
 });

+ 37 - 0
src/test/specs/dashboardViewStateSrv-specs.js

@@ -0,0 +1,37 @@
+define([
+  'services/dashboard/dashboardViewStateSrv'
+], function() {
+  'use strict';
+
+  describe('when updating view state', function() {
+    var viewState, location;
+
+    beforeEach(module('grafana.services'));
+
+    beforeEach(inject(function(dashboardViewStateSrv, $location, $rootScope) {
+      $rootScope.onAppEvent = function(){};
+      viewState = dashboardViewStateSrv.create($rootScope);
+      location = $location;
+    }));
+
+    describe('to fullscreen true and edit true', function() {
+      it('should update querystring and view state', function() {
+        var updateState = { fullscreen: true, edit: true, panelId: 1 };
+        viewState.update(updateState);
+        expect(location.search()).to.eql(updateState);
+        expect(viewState.fullscreen).to.be(true);
+      });
+    });
+
+    describe('to fullscreen false', function() {
+      it('should remove params from query string', function() {
+        viewState.update({fullscreen: true, panelId: 1, edit: true});
+        viewState.update({fullscreen: false});
+        expect(location.search()).to.eql({});
+        expect(viewState.fullscreen).to.be(false);
+      });
+    });
+
+  });
+
+});

+ 7 - 0
src/test/specs/helpers.js

@@ -26,6 +26,8 @@ define([
         self.scope.panel = {};
         self.scope.row = { panels:[] };
         self.scope.filter = new FilterSrvStub();
+        self.scope.dashboard = {};
+        self.scope.dashboardViewState = new DashboardViewStateStub();
 
         $rootScope.colors = [];
         for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
@@ -54,6 +56,11 @@ define([
     };
   }
 
+  function DashboardViewStateStub() {
+    this.registerPanel = function() {
+    };
+  }
+
   function FilterSrvStub() {
     this.time = { from:'now-1h', to: 'now'};
     this.timeRange = function(parse) {

+ 0 - 44
src/test/specs/row-ctrl-specs.js

@@ -12,50 +12,6 @@ define([
     beforeEach(ctx.providePhase());
     beforeEach(ctx.createControllerPhase('RowCtrl'));
 
-    describe('when getting rowSpan', function() {
-      it('should return sum of panels spans', function() {
-        var spanLeft = ctx.scope.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
-        expect(spanLeft).to.be(5);
-      });
-    });
-
-    describe('when adding panel to row with 12 span panel', function() {
-      it('should split span in half and add panel with defaults', function() {
-        ctx.scope.row = { panels: [{ span: 12 }] };
-        ctx.scope.add_panel_default('graph');
-
-        expect(ctx.scope.row.panels[0].span).to.be(6);
-        expect(ctx.scope.row.panels[1].span).to.be(6);
-        expect(ctx.scope.row.panels[1].type).to.be('graph');
-      });
-    });
-
-    describe('when duplicating panel', function() {
-      it('should try to add it to same row', function() {
-        var panel = { span: 4, attr: '123' };
-        ctx.scope.row = { panels: [panel] };
-        ctx.scope.duplicatePanel(panel, ctx.scope.row);
-
-        expect(ctx.scope.row.panels[0].span).to.be(4);
-        expect(ctx.scope.row.panels[1].span).to.be(4);
-        expect(ctx.scope.row.panels[1].attr).to.be('123');
-      });
-    });
-
-    describe('when duplicating panel', function() {
-      it('should add row if there is no space left', function() {
-        var panel = { span: 12, attr: '123' };
-        ctx.scope.row = { panels: [panel] };
-        ctx.scope.dashboard = { rows: [ctx.scope.row] };
-
-        ctx.scope.duplicatePanel(panel, ctx.scope.row);
-
-        expect(ctx.scope.row.panels[0].span).to.be(12);
-        expect(ctx.scope.row.panels.length).to.be(1);
-        expect(ctx.scope.dashboard.rows[1].panels[0].attr).to.be('123');
-      });
-    });
-
   });
 
 });

+ 1 - 0
src/test/test-main.js

@@ -124,6 +124,7 @@ require([
     'specs/filterSrv-specs',
     'specs/kbn-format-specs',
     'specs/dashboardSrv-specs',
+    'specs/dashboardViewStateSrv-specs',
     'specs/influxSeries-specs'
   ], function () {
     window.__karma__.start();