Browse Source

merged with oss

Torkel Ödegaard 11 years ago
parent
commit
dac4954215
55 changed files with 890 additions and 412 deletions
  1. 11 0
      CHANGELOG.md
  2. 18 8
      src/app/app.js
  3. 25 2
      src/app/components/kbn.js
  4. 2 1
      src/app/components/require.config.js
  5. 5 1
      src/app/components/settings.js
  6. 0 14
      src/app/controllers/console-ctrl.js
  7. 8 6
      src/app/controllers/dash.js
  8. 3 3
      src/app/controllers/dashLoader.js
  9. 65 3
      src/app/controllers/grafanaCtrl.js
  10. 33 59
      src/app/controllers/row.js
  11. 2 0
      src/app/controllers/search.js
  12. 0 35
      src/app/directives/addPanel.js
  13. 1 2
      src/app/directives/all.js
  14. 3 3
      src/app/directives/bodyClass.js
  15. 1 1
      src/app/directives/configModal.js
  16. 5 4
      src/app/directives/grafanaGraph.js
  17. 26 31
      src/app/directives/grafanaPanel.js
  18. 2 13
      src/app/directives/grafanaSimplePanel.js
  19. 1 1
      src/app/directives/grafanaVersionCheck.js
  20. 127 0
      src/app/p_app.js
  21. 1 3
      src/app/panels/graph/module.html
  22. 3 17
      src/app/panels/graph/module.js
  23. 1 1
      src/app/panels/text/module.html
  24. 15 20
      src/app/panels/text/module.js
  25. 2 9
      src/app/panels/timepicker/module.html
  26. 22 4
      src/app/panels/timepicker/module.js
  27. 6 4
      src/app/partials/dashLoader.html
  28. 13 8
      src/app/partials/dashboard.html
  29. 0 4
      src/app/partials/load.html
  30. 2 17
      src/app/partials/roweditor.html
  31. 1 1
      src/app/partials/search.html
  32. 1 0
      src/app/routes/dashboard-from-db.js
  33. 3 0
      src/app/routes/p_dashboard.js
  34. 1 0
      src/app/services/all.js
  35. 1 15
      src/app/services/dashboard/dashboardKeyBindings.js
  36. 85 5
      src/app/services/dashboard/dashboardSrv.js
  37. 156 0
      src/app/services/dashboard/dashboardViewStateSrv.js
  38. 2 0
      src/app/services/datasourceSrv.js
  39. 32 58
      src/app/services/panelSrv.js
  40. 1 0
      src/app/services/playlistSrv.js
  41. 6 0
      src/app/services/unsavedChangesSrv.js
  42. 5 1
      src/config.sample.js
  43. 1 1
      src/index.html
  44. 55 0
      src/plugins/datasource.example.js
  45. 65 4
      src/test/specs/dashboardSrv-specs.js
  46. 37 0
      src/test/specs/dashboardViewStateSrv-specs.js
  47. 7 0
      src/test/specs/helpers.js
  48. 14 0
      src/test/specs/kbn-format-specs.js
  49. 0 44
      src/test/specs/row-ctrl-specs.js
  50. 2 3
      src/test/test-main.js
  51. 8 3
      src/vendor/angular/angular-dragdrop.js
  52. 2 1
      src/vendor/angular/angular-strap.js
  53. 1 1
      src/vendor/bootstrap/bootstrap.js
  54. 0 1
      src/vendor/jquery/jquery-1.8.0.js
  55. 1 0
      src/vendor/jquery/jquery-2.1.1.min.js

+ 11 - 0
CHANGELOG.md

@@ -3,12 +3,23 @@
 **New features and improvements**
 
 - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
+- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
+
+**Fixes**
+- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
 
 **Tech**
 - Upgraded from angularjs 1.1.5 to 1.3 beta 17;
 - Switch from underscore to lodash
 - helpers to easily unit test angularjs controllers and services
 - Test coverage through coveralls
+- Upgrade from jquery 1.8.0 to 2.1.1 (**Removes support for IE7 & IE8**)
+
+# 1.7.1 (unreleased)
+
+**Fixes**
+- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
+- [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
 
 # 1.7.0 (2014-08-11)
 

+ 18 - 8
src/app/app.js

@@ -14,7 +14,7 @@ define([
   'extend-jquery',
   'bindonce',
 ],
-function (angular, $, _, appLevelRequire) {
+function (angular, $, _, appLevelRequire, config) {
 
   "use strict";
 
@@ -56,6 +56,7 @@ function (angular, $, _, appLevelRequire) {
     register_fns.factory    = $provide.factory;
     register_fns.service    = $provide.service;
     register_fns.filter     = $filterProvider.register;
+
   });
 
   var apps_deps = [
@@ -76,14 +77,23 @@ function (angular, $, _, appLevelRequire) {
     apps_deps.push(module_name);
   });
 
+  var preBootRequires = [
+    'controllers/all',
+    'directives/all',
+    'filters/all',
+    'components/partials',
+    'routes/all',
+  ];
+
+  _.each(config.plugins.dependencies, function(dep) {
+    preBootRequires.push('../plugins/' + dep);
+  });
+
   app.boot = function() {
-    require([
-      'controllers/all',
-      'directives/all',
-      'filters/all',
-      'components/partials',
-      'routes/p_all',
-    ], function () {
+    require(preBootRequires, function () {
+
+      // disable tool tip animation
+      $.fn.tooltip.defaults.animation = false;
 
       // bootstrap the app
       angular

+ 25 - 2
src/app/components/kbn.js

@@ -525,12 +525,35 @@ function($, _, moment) {
         return kbn.nanosFormat(val, decimals);
       };
     default:
-      return function(val) {
-        return val % 1 === 0 ? val : val.toFixed(decimals);
+      return function(val, axis) {
+        return kbn.noneFormat(val, axis ? axis.tickDecimals : decimals);
       };
     }
   };
 
+  kbn.noneFormat = function(value, decimals) {
+    var factor = decimals ? Math.pow(10, decimals) : 1;
+    var formatted = String(Math.round(value * factor) / factor);
+
+    // if exponent return directly
+    if (formatted.indexOf('e') !== -1) {
+      return formatted;
+    }
+
+    // If tickDecimals was specified, ensure that we have exactly that
+    // much precision; otherwise default to the value's own precision.
+
+    if (decimals != null) {
+      var decimalPos = formatted.indexOf(".");
+      var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
+      if (precision < decimals) {
+        return (precision ? formatted : formatted + ".") + (String(factor)).substr(1, decimals - precision);
+      }
+    }
+
+    return formatted;
+  };
+
   kbn.msFormat = function(size, decimals) {
     // Less than 1 milli, downscale to micro
     if (Math.abs(size) < 1) {

+ 2 - 1
src/app/components/require.config.js

@@ -5,6 +5,7 @@ require.config({
   baseUrl: '/public/app',
 
   paths: {
+    app: 'p_app',
     config:                   ['../config', '../config.sample'],
     settings:                 'components/settings',
     kbn:                      'components/kbn',
@@ -27,7 +28,7 @@ require.config({
     'lodash-src':             '../vendor/lodash',
     bootstrap:                '../vendor/bootstrap/bootstrap',
 
-    jquery:                   '../vendor/jquery/jquery-1.8.0',
+    jquery:                   '../vendor/jquery/jquery-2.1.1.min',
     'jquery-ui':              '../vendor/jquery/jquery-ui-1.10.3',
 
     'extend-jquery':          'components/extend-jquery',

+ 5 - 1
src/app/components/settings.js

@@ -70,7 +70,7 @@ function (_, crypto) {
 
     _.each(settings.datasources, function(datasource, key) {
       datasource.name = key;
-      parseBasicAuth(datasource);
+      if (datasource.url) { parseBasicAuth(datasource); }
       if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
     });
 
@@ -78,6 +78,10 @@ function (_, crypto) {
       settings.panels = _.union(settings.panels, settings.plugins.panels);
     }
 
+    if (!settings.plugins.dependencies) {
+      settings.plugins.dependencies = [];
+    }
+
     return settings;
   };
 });

+ 0 - 14
src/app/controllers/console-ctrl.js

@@ -15,20 +15,6 @@ function (angular, _, moment) {
 
   var events = [];
 
-  var oldLog = console.log;
-  console.log = function (message) {
-    try {
-      if (_.isObject(message)) {
-        message = angular.toJson(message);
-        if (message.length > 50) {
-          message = message.substring(0, 50);
-        }
-      }
-      events.push(new ConsoleEvent('log', message, {}));
-      oldLog.apply(console, arguments);
-    } catch (e) { }
-  };
-
   function ConsoleEvent(type, title, data) {
     this.type = type;
     this.title = title;

+ 8 - 6
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,13 @@ function (angular, $, config, _) {
     $scope.setupDashboard = function(event, dashboardData) {
       timer.cancel_all();
 
-      $rootScope.fullscreen = false;
+      $rootScope.performance.dashboardLoadStart = new Date().getTime();
+      $rootScope.performance.panelsInitialized = 0;
+      $rootScope.performance.panelsRendered= 0;
 
       $scope.dashboard = dashboardSrv.create(dashboardData);
+      $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
+
       $scope.grafana.style = $scope.dashboard.style;
 
       $scope.filter = filterSrv;
@@ -78,10 +84,6 @@ function (angular, $, config, _) {
       };
     };
 
-    $scope.row_style = function(row) {
-      return { 'min-height': row.collapse ? '5px' : row.height };
-    };
-
     $scope.panel_path =function(type) {
       if(type) {
         return 'app/panels/'+type.replace(".","/");

+ 3 - 3
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();
       });
@@ -21,10 +22,7 @@ function (angular, _, moment, config) {
       $scope.onAppEvent('zoom-out', function() {
         $scope.zoom(2);
       });
-    };
 
-    $scope.exitFullscreen = function() {
-      $scope.emitAppEvent('panel-fullscreen-exit');
     };
 
     $scope.set_default = function() {
@@ -78,6 +76,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);
@@ -135,6 +134,7 @@ function (angular, _, moment, config) {
 
     $scope.openSaveDropdown = function() {
       $scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
+      $scope.saveDropdownOpened = true;
     };
 
     $scope.markAsFavorite = function() {

+ 65 - 3
src/app/controllers/grafanaCtrl.js

@@ -2,8 +2,9 @@ define([
   'angular',
   'config',
   'lodash',
+  'jquery',
 ],
-function (angular, config, _) {
+function (angular, config, _, $) {
   "use strict";
 
   var module = angular.module('grafana.controllers');
@@ -11,15 +12,21 @@ function (angular, config, _) {
   module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
 
     $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
+    $scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
+
+    $rootScope.profilingEnabled = (window.localStorage && window.localStorage.profilingEnabled === 'true');
+    $rootScope.performance = { loadStart: new Date().getTime() };
 
     $scope.init = function() {
       $scope._ = _;
+      if ($rootScope.profilingEnabled) {
+        $scope.initProfiling();
+      }
+
       $scope.dashAlerts = alertSrv;
       $scope.grafana = {
         style: 'dark'
       };
-
-      $scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
     };
 
     $scope.toggleConsole = function() {
@@ -46,6 +53,61 @@ function (angular, config, _) {
       "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"  //7
     ];
 
+    $scope.getTotalWatcherCount = function() {
+      var count = 0;
+      var scopes = 0;
+      var root = $(document.getElementsByTagName('body'));
+
+      var f = function (element) {
+        if (element.data().hasOwnProperty('$scope')) {
+          scopes++;
+          angular.forEach(element.data().$scope.$$watchers, function () {
+            count++;
+          });
+        }
+
+        angular.forEach(element.children(), function (childElement) {
+          f($(childElement));
+        });
+      };
+
+      f(root);
+      $rootScope.performance.scopeCount = scopes;
+      return count;
+    };
+
+    $scope.initProfiling = function() {
+      var count = 0;
+
+      $scope.$watch(function digestCounter() {
+        count++;
+      }, function() {
+      });
+
+      $scope.onAppEvent('setup-dashboard', function() {
+        count = 0;
+
+        setTimeout(function() {
+          console.log("Dashboard::Performance Total Digests: " + count);
+          console.log("Dashboard::Performance Total Watchers: " + $scope.getTotalWatcherCount());
+          console.log("Dashboard::Performance Total ScopeCount: " + $rootScope.performance.scopeCount);
+
+          var timeTaken = $rootScope.performance.allPanelsInitialized - $rootScope.performance.dashboardLoadStart;
+          console.log("Dashboard::Performance - All panels initialized in " + timeTaken + " ms");
+
+          // measure digest performance
+          var rootDigestStart = window.performance.now();
+          for (var i = 0; i < 30; i++) {
+            $rootScope.$apply();
+          }
+          console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
+
+        }, 3000);
+
+      });
+
+    };
+
     $scope.init();
 
   });

+ 33 - 59
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
     $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,12 +104,37 @@ function (angular, app, _) {
       $scope.row.height = fixRowHeight($scope.row.height);
     };
 
-    /** @scratch /panels/2
-     * --
-     */
-
     $scope.init();
 
   });
 
+  module.directive('rowHeight', function() {
+    return function(scope, element) {
+      scope.$watchGroup(['row.collapse', 'row.height'], function() {
+        element[0].style.minHeight = scope.row.collapse ? '5px' : scope.row.height;
+      });
+    };
+  });
+
+  module.directive('panelWidth', function() {
+    return function(scope, element) {
+      scope.$watch('panel.span', function() {
+        element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
+      });
+    };
+  });
+
+  module.directive('panelDropZone', function() {
+    return function(scope, element) {
+      scope.$watch('dashboard.$$panelDragging', function(newVal) {
+        if (newVal && scope.dashboard.rowSpan(scope.row) < 10) {
+          element.show();
+        }
+        else {
+          element.hide();
+        }
+      });
+    };
+  });
+
 });

+ 2 - 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;
@@ -98,6 +99,7 @@ function (angular, _, config, $) {
         $element.next().find('.dropdown-toggle').dropdown('toggle');
       }
 
+      $scope.searchOpened = true;
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
       $scope.query.query = 'title:';
       $scope.search();

+ 0 - 35
src/app/directives/addPanel.js

@@ -1,35 +0,0 @@
-define([
-  'angular',
-  'app',
-  'lodash'
-],
-function (angular, app, _) {
-  'use strict';
-
-  angular
-    .module('grafana.directives')
-    .directive('addPanel', function($compile) {
-      return {
-        restrict: 'A',
-        link: function($scope, elem) {
-
-          $scope.$on("$destroy",function() {
-            elem.remove();
-          });
-
-          $scope.$watch('panel.type', function() {
-            var _type = $scope.panel.type;
-            $scope.reset_panel(_type);
-            if(!_.isUndefined($scope.panel.type)) {
-              $scope.panel.loadingEditor = true;
-              $scope.require(['panels/'+$scope.panel.type.replace(".","/") +'/module'], function () {
-                var template = '<div ng-controller="'+$scope.panel.type+'" ng-include="\'app/partials/paneladd.html\'"></div>';
-                elem.html($compile(angular.element(template))($scope));
-                $scope.panel.loadingEditor = false;
-              });
-            }
-          });
-        }
-      };
-    });
-});

+ 1 - 2
src/app/directives/all.js

@@ -1,5 +1,4 @@
 define([
-  './addPanel',
   './arrayJoin',
   './dashUpload',
   './grafanaPanel',
@@ -17,4 +16,4 @@ define([
   './graphiteFuncEditor',
   './grafanaVersionCheck',
   './influxdbFuncEditor'
-], function () {});
+], function () {});

+ 3 - 3
src/app/directives/bodyClass.js

@@ -15,7 +15,7 @@ function (angular, app, _) {
           var lastPulldownVal;
           var lastHideControlsVal;
 
-          $scope.$watch('dashboard.pulldowns', function() {
+          $scope.$watchCollection('dashboard.pulldowns', function() {
             if (!$scope.dashboard) {
               return;
             }
@@ -26,7 +26,7 @@ function (angular, app, _) {
               elem.toggleClass('submenu-controls-visible', panelEnabled);
               lastPulldownVal = panelEnabled;
             }
-          }, true);
+          });
 
           $scope.$watch('dashboard.hideControls', function() {
             if (!$scope.dashboard) {
@@ -49,4 +49,4 @@ function (angular, app, _) {
       };
     });
 
-});
+});

+ 1 - 1
src/app/directives/configModal.js

@@ -45,4 +45,4 @@ function (angular, _, $) {
         }
       };
     });
-});
+});

+ 5 - 4
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();
         });
@@ -300,9 +303,7 @@ function (angular, $, kbn, moment, _) {
         }
 
         function configureAxisMode(axis, format) {
-          if (format !== 'none') {
-            axis.tickFormatter = kbn.getFormatFunction(format, 1);
-          }
+          axis.tickFormatter = kbn.getFormatFunction(format, 1);
         }
 
         function time_format(interval, ticks, min, max) {

+ 26 - 31
src/app/directives/grafanaPanel.js

@@ -8,7 +8,7 @@ function (angular, $) {
 
   angular
     .module('grafana.directives')
-    .directive('grafanaPanel', function($compile) {
+    .directive('grafanaPanel', function($compile, $parse) {
 
       var container = '<div class="panel-container"></div>';
       var content = '<div class="panel-content"></div>';
@@ -18,11 +18,13 @@ function (angular, $) {
        '<div class="row-fluid panel-extra">' +
           '<div class="panel-extra-container">' +
             '<span class="alert-error panel-error small pointer"' +
-                  'config-modal="app/partials/inspector.html" ng-show="panel.error" data-placement="right" bs-tooltip="panel.error">' +
+                  'config-modal="app/partials/inspector.html" ng-if="panel.error">' +
+              '<span data-placement="right" bs-tooltip="panel.error">' +
               '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
+              '</span>' +
             '</span>' +
 
-            '<span class="panel-loading" ng-show="panelMeta.loading == true">' +
+            '<span class="panel-loading" ng-show="panelMeta.loading">' +
               '<i class="icon-spinner icon-spin icon-large"></i>' +
             '</span>' +
 
@@ -36,7 +38,7 @@ function (angular, $) {
                 'index:{{$index}},'+
                 'onStart:\'panelMoveStart\','+
                 'onStop:\'panelMoveStop\''+
-                '}"  ng-model="row.panels" ' +
+                '}"  ng-model="panel" ' +
                 '>' +
                 '{{panel.title || "No title"}}' +
               '</span>' +
@@ -49,8 +51,7 @@ function (angular, $) {
       return {
         restrict: 'E',
         link: function($scope, elem, attr) {
-          // once we have the template, scan it for controllers and
-          // load the module.js if we have any
+          var getter = $parse(attr.type), panelType = getter($scope);
           var newScope = $scope.$new();
 
           $scope.kbnJqUiDraggableOptions = {
@@ -68,6 +69,14 @@ function (angular, $) {
             /* jshint indent:false */
             $compile(elem.contents())(newScope);
             elem.removeClass("ng-cloak");
+
+            var panelCtrlElem = $(elem.children()[0]);
+            var panelCtrlScope = panelCtrlElem.data().$scope;
+
+            panelCtrlScope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
+              panelCtrlElem.css({ minHeight: panelCtrlScope.panel.height || panelCtrlScope.row.height });
+              panelCtrlElem.toggleClass('panel-fullscreen', panelCtrlScope.fullscreen ? true : false);
+            });
           }
 
           newScope.$on('$destroy',function() {
@@ -75,31 +84,17 @@ function (angular, $) {
             elem.remove();
           });
 
-          $scope.$watch(attr.type, function (name) {
-            elem.addClass("ng-cloak");
-            // load the panels module file, then render it in the dom.
-            var nameAsPath = name.replace(".", "/");
-            $scope.require([
-              'jquery',
-              'text!panels/'+nameAsPath+'/module.html'
-            ], function ($, moduleTemplate) {
-              var $module = $(moduleTemplate);
-              // top level controllers
-              var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
-              // add child controllers
-              $controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
-
-              if ($controllers.length) {
-                $controllers.first().prepend(panelHeader);
-                $controllers.first().find('.panel-header').nextAll().wrapAll(content);
-
-                $scope.require(['panels/' + nameAsPath + '/module'], function() {
-                  loadModule($module);
-                });
-              } else {
-                loadModule($module);
-              }
-            });
+          elem.addClass('ng-cloak');
+
+          $scope.require([
+            'jquery',
+            'text!panels/'+panelType+'/module.html',
+            'panels/' + panelType + "/module",
+          ], function ($, moduleTemplate) {
+            var $module = $(moduleTemplate);
+            $module.prepend(panelHeader);
+            $module.first().find('.panel-header').nextAll().wrapAll(content);
+            loadModule($module);
           });
 
         }

+ 2 - 13
src/app/directives/grafanaSimplePanel.js

@@ -1,8 +1,7 @@
 define([
   'angular',
-  'lodash'
 ],
-function (angular, _) {
+function (angular) {
   'use strict';
 
   angular
@@ -60,18 +59,8 @@ function (angular, _) {
             loadController(name);
           });
 
-          if(attr.panel) {
-            $scope.$watch(attr.panel, function (panel) {
-              // If the panel attribute is specified, create a new scope. This ruins configuration
-              // so don't do it with anything that needs to use editor.html
-              if(!_.isUndefined(panel)) {
-                $scope = $scope.$new();
-                $scope.panel = angular.fromJson(panel);
-              }
-            });
-          }
         }
       };
     });
 
-});
+});

+ 1 - 1
src/app/directives/grafanaVersionCheck.js

@@ -14,7 +14,7 @@ function (angular) {
             return;
           }
 
-          $http({ method: 'GET', url: 'http://grafanarel.s3.amazonaws.com/latest.json' })
+          $http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' })
             .then(function(response) {
               if (!response.data || !response.data.version) {
                 return;

+ 127 - 0
src/app/p_app.js

@@ -0,0 +1,127 @@
+/**
+ * main app level module
+ */
+define([
+  'angular',
+  'jquery',
+  'lodash',
+  'require',
+  'config',
+  'bootstrap',
+  'angular-route',
+  'angular-strap',
+  'angular-dragdrop',
+  'extend-jquery',
+  'bindonce',
+],
+function (angular, $, _, appLevelRequire, config) {
+
+  "use strict";
+
+  var app = angular.module('grafana', []),
+    // we will keep a reference to each module defined before boot, so that we can
+    // go back and allow it to define new features later. Once we boot, this will be false
+    pre_boot_modules = [],
+    // these are the functions that we need to call to register different
+    // features if we define them after boot time
+    register_fns = {};
+
+  // This stores the grafana version number
+  app.constant('grafanaVersion',"@grafanaVersion@");
+
+  // Use this for cache busting partials
+  app.constant('cacheBust',"cache-bust="+Date.now());
+
+  /**
+   * Tells the application to watch the module, once bootstraping has completed
+   * the modules controller, service, etc. functions will be overwritten to register directly
+   * with this application.
+   * @param  {[type]} module [description]
+   * @return {[type]}        [description]
+   */
+  app.useModule = function (module) {
+    if (pre_boot_modules) {
+      pre_boot_modules.push(module);
+    } else {
+      _.extend(module, register_fns);
+    }
+    return module;
+  };
+
+  app.config(function ($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
+    $locationProvider.html5Mode(true);
+    // this is how the internet told me to dynamically add modules :/
+    register_fns.controller = $controllerProvider.register;
+    register_fns.directive  = $compileProvider.directive;
+    register_fns.factory    = $provide.factory;
+    register_fns.service    = $provide.service;
+    register_fns.filter     = $filterProvider.register;
+  });
+
+  var apps_deps = [
+    'ngRoute',
+    '$strap.directives',
+    'ngDragDrop',
+    'grafana',
+    'pasvaz.bindonce'
+  ];
+
+  var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
+
+  _.each(module_types, function (type) {
+    var module_name = 'grafana.'+type;
+    // create the module
+    app.useModule(angular.module(module_name, []));
+    // push it into the apps dependencies
+    apps_deps.push(module_name);
+  });
+
+  var preBootRequires = [
+    'controllers/all',
+    'directives/all',
+    'filters/all',
+    'components/partials',
+    'routes/p_all',
+  ];
+
+  _.each(config.plugins.dependencies, function(dep) {
+    preBootRequires.push('../plugins/' + dep);
+  });
+
+  app.boot = function() {
+    require(preBootRequires, function () {
+
+      // disable tool tip animation
+      $.fn.tooltip.defaults.animation = false;
+
+      // bootstrap the app
+      angular
+        .element(document)
+        .ready(function() {
+          angular.bootstrap(document, apps_deps)
+            .invoke(['$rootScope', function ($rootScope) {
+              _.each(pre_boot_modules, function (module) {
+                _.extend(module, register_fns);
+              });
+              pre_boot_modules = false;
+
+              $rootScope.requireContext = appLevelRequire;
+              $rootScope.require = function (deps, fn) {
+                var $scope = this;
+                $scope.requireContext(deps, function () {
+                  var deps = _.toArray(arguments);
+                  // Check that this is a valid scope.
+                  if($scope.$id) {
+                    $scope.$apply(function () {
+                      fn.apply($scope, deps);
+                    });
+                  }
+                });
+              };
+            }]);
+        });
+    });
+  };
+
+  return app;
+});

+ 1 - 3
src/app/panels/graph/module.html

@@ -1,6 +1,4 @@
-<div  ng-controller='GraphCtrl'
-      style="min-height:{{panel.height || row.height}}"
-      ng-class="{'panel-fullscreen': fullscreen}">
+<div  ng-controller='GraphCtrl'>
 
   <div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
       <div class="graph-canvas-wrapper">

+ 3 - 17
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 = {
@@ -253,7 +243,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
 
       var data = _.map(results.data, $scope.seriesHandler);
 
-      $scope.datapointsWarning = $scope.datapointsCount || !$scope.datapointsOutside;
+      $scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
 
       $scope.annotationsPromise
         .then(function(annotations) {
@@ -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/panels/text/module.html

@@ -1,4 +1,4 @@
-<div ng-controller='text' style="min-height:{{panel.height || row.height}}">
+<div ng-controller='text'>
   <p ng-bind-html="content">
   </p>
 </div>

+ 15 - 20
src/app/panels/text/module.js

@@ -1,15 +1,3 @@
-/** @scratch /panels/5
- * include::panels/text.asciidoc[]
- */
-
-/** @scratch /panels/text/0
- * == text
- * Status: *Stable*
- *
- * The text panel is used for displaying static text formated as markdown, sanitized html or as plain
- * text.
- *
- */
 define([
   'angular',
   'app',
@@ -23,6 +11,8 @@ function (angular, app, _, require) {
   var module = angular.module('grafana.panels.text', []);
   app.useModule(module);
 
+  var converter;
+
   module.controller('text', function($scope, filterSrv, $sce, panelSrv) {
 
     $scope.panelMeta = {
@@ -68,15 +58,20 @@ function (angular, app, _, require) {
     };
 
     $scope.renderMarkdown = function(content) {
-      require(['./lib/showdown'], function (Showdown) {
-        var converter = new Showdown.converter();
-        var text = content
-          .replace(/&/g, '&amp;')
-          .replace(/>/g, '&gt;')
-          .replace(/</g, '&lt;');
+      var text = content
+        .replace(/&/g, '&amp;')
+        .replace(/>/g, '&gt;')
+        .replace(/</g, '&lt;');
 
+      if (converter) {
         $scope.updateContent(converter.makeHtml(text));
-      });
+      }
+      else {
+        require(['./lib/showdown'], function (Showdown) {
+          converter = new Showdown.converter();
+          $scope.updateContent(converter.makeHtml(text));
+        });
+      }
     };
 
     $scope.updateContent = function(html) {
@@ -88,7 +83,7 @@ function (angular, app, _, require) {
       }
 
       if(!$scope.$$phase) {
-        $scope.$apply();
+        $scope.$digest();
       }
     };
 

+ 2 - 9
src/app/panels/timepicker/module.html

@@ -15,15 +15,8 @@
     <ul class="nav nav-pills timepicker-dropdown">
       <li class="dropdown">
 
-        <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.from.date ? (time.from.date | date:'yyyy-MM-dd HH:mm:ss.sss') + ' <br>to<br>' +(time.to.date | date:'yyyy-MM-dd HH:mm:ss.sss') : 'Click to set a time filter'" data-placement="bottom" ng-click="dismiss();">
-
-          <span ng-show="filter.time">
-            <span class="pointer" ng-hide="panel.now">{{time.from.date | date:'MMM d, y HH:mm:ss'}}</span>
-            <span class="pointer" ng-show="panel.now">{{time.from.date | moment:'ago'}}</span>
-            to
-            <span class="pointer" ng-hide="panel.now" >{{time.to.date | date:'MMM d, y HH:mm:ss'}}</span>
-            <span class="pointer" ng-show="panel.now">{{time.to.date | moment:'ago'}}</span>
-          </span>
+        <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
+          <span ng-show="filter.time" ng-bind="time.rangeString"></span>
           <span ng-hide="filter.time">Time filter</span>
           <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
           <i class="icon-caret-down"></i>

+ 22 - 4
src/app/panels/timepicker/module.js

@@ -172,10 +172,28 @@ function (angular, app, _, moment, kbn) {
     };
 
     var getScopeTimeObj = function(from,to) {
-      return {
-        from: getTimeObj(from),
-        to: getTimeObj(to)
-      };
+      var model = { from: getTimeObj(from), to: getTimeObj(to), };
+
+      if (model.from.date) {
+        model.tooltip = moment(model.from.date).format('YYYY-MM-DD HH:mm:ss') + ' <br>to<br>';
+        model.tooltip += moment(model.to.date).format('YYYY-MM-DD HH:mm:ss');
+      }
+      else {
+        model.tooltip = 'Click to set time filter';
+      }
+
+      if ($scope.filter.time) {
+        if ($scope.panel.now) {
+          model.rangeString = moment(model.from.date).fromNow() + ' to ' +
+            moment(model.to.date).fromNow();
+        }
+        else {
+          model.rangeString = moment(model.from.date).format('MMM D, YYYY hh:mm:ss') + ' to ' +
+            moment(model.to.date).format('MMM D, YYYY hh:mm:ss');
+        }
+      }
+
+      return model;
     };
 
     var getTimeObj = function(date) {

+ 6 - 4
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>
@@ -16,15 +16,17 @@
   </a>
 </li>
 
-<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable"><grafana-simple-panel type="pulldown.type" ng-cloak></grafana-simple-panel></li>
+<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
+	<grafana-simple-panel type="pulldown.type" ng-cloak>
+	</grafana-simple-panel>
+</li>
 
 <li class="dropdown grafana-menu-save">
   <a href="#"  bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="openSaveDropdown()">
     <i class='icon-save'></i>
   </a>
 
-  <ul class="save-dashboard-dropdown dropdown-menu">
-
+  <ul class="save-dashboard-dropdown dropdown-menu" ng-if="saveDropdownOpened">
     <li>
       <form class="input-prepend nomargin save-dashboard-dropdown-save-form">
         <input class='input-medium' ng-model="dashboard.title" type="text" />

+ 13 - 8
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">
@@ -27,7 +27,7 @@
     <div>
       <div class="grafana-container container">
         <!-- Rows -->
-        <div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" ng-style="row_style(row)">
+        <div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
           <div class="row-control">
             <div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
               <div class="row-close" ng-show="row.collapse" data-placement="bottom" >
@@ -98,14 +98,19 @@
 							</div>
 
               <!-- Panels -->
-              <div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.hide" class="panel nospace" ng-style="{'width':(panel.span/1.2)*10+'%'}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}" ng-class="{'dragInProgress':dashboard.$$panelDragging}">
-                <!-- Content Panel -->
-                <div style="position:relative">
-                  <grafana-panel type="panel.type" ng-cloak></grafana-panel>
-                </div>
+							<div ng-repeat="(name, panel) in row.panels"
+									 class="panel nospace"
+									 style="position:relative"
+									 data-drop="true"
+									 panel-width
+									 ng-model="panel"
+									 data-jqyoui-options
+									 jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
+									 ng-class="{'dragInProgress':dashboard.$$panelDragging}">
+                <grafana-panel type="panel.type" ng-cloak></grafana-panel>
               </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 panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" 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>

+ 0 - 4
src/app/partials/load.html

@@ -1,4 +0,0 @@
-<div style="margin-top:50px" ng-controller="dashcontrol">
-    <strong>type: </strong>{{type}} <br>
-    <strong>id: </strong>{{id}} <br>
-</div>

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

@@ -2,7 +2,7 @@
   <div class="pull-right editor-title">Row settings</div>
 
   <div ng-model="editor.index" bs-tabs>
-    <div ng-repeat="tab in ['General','Panels','Add Panel']" data-title="{{tab}}">
+    <div ng-repeat="tab in ['General','Panels']" data-title="{{tab}}">
     </div>
   </div>
 
@@ -27,11 +27,10 @@
         <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>
-          <th>Hide</th>
         </thead>
         <tr ng-repeat="panel in row.panels">
           <td>{{panel.title}}</td>
@@ -40,24 +39,10 @@
           <td><i ng-click="row.panels = _.without(row.panels,panel)" class="pointer icon-remove"></i></td>
           <td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
           <td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
-          <td><input type="checkbox" ng-model="panel.hide" ng-checked="panel.hide"></td>
         </tr>
       </table>
     </div>
   </div>
-  <div class="row-fluid" ng-if="editor.index == 2">
-    <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">
-        Note: This row is full, new panels will wrap to a new line. You should add another row.
-      </small>
-    </form>
-
-    <div ng-show="!(_.isUndefined(panel.type))">
-      <div add-panel="{{panel.type}}"></div>
-    </div>
-  </div>
 </div>
 <div class="modal-footer">
   <button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>

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

@@ -16,7 +16,7 @@
     <i class='icon-folder-open'></i>
   </a>
 
-  <ul class="dropdown-menu" id="grafana-search">
+  <ul class="dropdown-menu" id="grafana-search" ng-if="searchOpened">
     <li ng-if="!showImport">
       <div class="grafana-search-panel">
         <div class="search-field-wrapper">

+ 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',

+ 3 - 0
src/app/routes/p_dashboard.js

@@ -12,14 +12,17 @@ function (angular) {
       .when('/', {
         templateUrl: '/app/partials/dashboard.html',
         controller : 'DashFromDBProvider',
+        reloadOnSearch: false,
       })
       .when('/dashboard/db/:id', {
         templateUrl: '/app/partials/dashboard.html',
         controller : 'DashFromDBProvider',
+        reloadOnSearch: false,
       })
       .when('/dashboard/temp/:id', {
         templateUrl: '/app/partials/dashboard.html',
         controller : 'DashFromDBProvider',
+        reloadOnSearch: false,
       })
       .when('/login', {
         templateUrl: '/app/partials/p_login.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 {

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

@@ -0,0 +1,156 @@
+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();
+        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);
+      }
+    };
+
+  });
+});

+ 2 - 0
src/app/services/datasourceSrv.js

@@ -68,6 +68,8 @@ function (angular, _, config) {
       case 'grafana':
         Datasource = $injector.get('GrafanaDatasource');
         break;
+      default:
+        Datasource = $injector.get(ds.type);
       }
       return new Datasource(ds);
     };

+ 32 - 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,31 @@ 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();
+        }
+      }
+
+      if ($rootScope.profilingEnabled) {
+        $rootScope.performance.panelsInitialized++;
+        if ($rootScope.performance.panelsInitialized === $scope.dashboard.rows.length) {
+          $rootScope.performance.allPanelsInitialized = new Date().getTime();
+        }
+      }
     };
   });
 

+ 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++;
         });

+ 6 - 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,10 @@ function(angular, _, config) {
 
     this.init = function() {
       $rootScope.$on("$locationChangeStart", function(event, next) {
+        if (self.originalPath === $location.path()) {
+          return;
+        }
+
         if (self.has_unsaved_changes()) {
           event.preventDefault();
           self.next = next;

+ 5 - 1
src/config.sample.js

@@ -96,7 +96,11 @@ function (Settings) {
 
     // Add your own custom pannels
     plugins: {
-      panels: []
+      // list of plugin panels
+      panels: [],
+      // requirejs modules in plugins folder that should be loaded
+      // for example custom datasources
+      dependencies: [],
     }
 
   });

+ 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>

+ 55 - 0
src/plugins/datasource.example.js

@@ -0,0 +1,55 @@
+define([
+  'angular',
+  'lodash',
+  'kbn',
+  'moment'
+],
+function (angular, _, kbn) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.factory('CustomDatasource', function($q) {
+
+    // the datasource object passed to constructor
+    // is the same defined in config.js
+    function CustomDatasource(datasource) {
+      this.name = datasource.name;
+      this.supportMetrics = true;
+      this.url = datasource.url;
+    }
+
+    CustomDatasource.prototype.query = function(filterSrv, options) {
+      // get from & to in seconds
+      var from = kbn.parseDate(options.range.from).getTime() / 1000;
+      var to = kbn.parseDate(options.range.to).getTime() / 1000;
+
+      var series = [];
+      var stepInSeconds = (to - from) / options.maxDataPoints;
+
+      for (var i = 0; i < 3; i++) {
+        var walker = Math.random() * 100;
+        var time = from;
+        var timeSeries = {
+          target: "Series " + i,
+          datapoints: []
+        };
+
+        for (var j = 0; j < options.maxDataPoints; j++) {
+          timeSeries.datapoints[j] = [walker, time];
+          walker += Math.random() - 0.5;
+          time += stepInSeconds;
+        }
+
+        series.push(timeSeries);
+      }
+
+      return $q.when({data: series });
+
+    };
+
+    return CustomDatasource;
+
+  });
+
+});

+ 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) {

+ 14 - 0
src/test/specs/kbn-format-specs.js

@@ -22,6 +22,20 @@ define([
 
   });
 
+  describe('high negative exponent, issue #696', function() {
+    it('should ignore decimal correction if exponent', function() {
+      var str = kbn.getFormatFunction('')(2.75e-10, { tickDecimals: 12 });
+      expect(str).to.be('2.75e-10');
+    });
+  });
+
+  describe('none format tests', function() {
+    it('should translate 2 as 2.0000 if axis decimals is 4', function() {
+      var str = kbn.getFormatFunction('')(2, { tickDecimals: 4 });
+      expect(str).to.be('2.0000');
+    });
+  });
+
   describe('nanosecond formatting', function () {
 
     it('should translate 25 to 25 ns', function () {

+ 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');
-      });
-    });
-
   });
 
 });

+ 2 - 3
src/test/test-main.js

@@ -26,7 +26,7 @@ require.config({
     crypto:                   '../vendor/crypto.min',
     spectrum:                 '../vendor/spectrum',
 
-    jquery:                   '../vendor/jquery/jquery-1.8.0',
+    jquery:                   '../vendor/jquery/jquery-2.1.1.min',
 
     bootstrap:                '../vendor/bootstrap/bootstrap',
     'bootstrap-tagsinput':    '../vendor/tagsinput/bootstrap-tagsinput',
@@ -124,8 +124,7 @@ require([
     'specs/filterSrv-specs',
     'specs/kbn-format-specs',
     'specs/dashboardSrv-specs',
-    'specs/influxSeries-specs',
-    'specs/overview-ctrl-specs',
+    'specs/influxSeries-specs'
   ], function () {
     window.__karma__.start();
   });

+ 8 - 3
src/vendor/angular/angular-dragdrop.js

@@ -257,9 +257,11 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
       require: '?jqyouiDroppable',
       restrict: 'A',
       link: function(scope, element, attrs) {
-        var dragSettings, zIndex;
+        // grafana change, remove watcher after first evaluation
+        var dragSettings, zIndex, removeWatcher;
         var updateDraggable = function(newValue, oldValue) {
           if (newValue) {
+            removeWatcher();
             dragSettings = scope.$eval(element.attr('jqyoui-draggable')) || [];
             element
               .draggable({disabled: false})
@@ -283,7 +285,7 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
             element.draggable({disabled: true});
           }
         };
-        scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
+        removeWatcher = scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
         updateDraggable();
       }
     };
@@ -292,8 +294,11 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
       restrict: 'A',
       priority: 1,
       link: function(scope, element, attrs) {
+        // grafana change, remove watcher after first evaluation
+        var removeWatcher;
         var updateDroppable = function(newValue, oldValue) {
           if (newValue) {
+            removeWatcher();
             element
               .droppable({disabled: false})
               .droppable(scope.$eval(attrs.jqyouiOptions) || {})
@@ -319,7 +324,7 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
           }
         };
 
-        scope.$watch(function() { return scope.$eval(attrs.drop); }, updateDroppable);
+        removeWatcher = scope.$watch(function() { return scope.$eval(attrs.drop); }, updateDroppable);
         updateDroppable();
       }
     };

+ 2 - 1
src/vendor/angular/angular-strap.js

@@ -783,7 +783,8 @@ angular.module('$strap.directives').directive('bsTooltip', [
             value = newValue;
           }
         });
-        if (!!attrs.unique) {
+        // Grafana change, always hide other tooltips
+        if (true) {
           element.on('show', function (ev) {
             $('.tooltip.in').each(function () {
               var $this = $(this), tooltip = $this.data('tooltip');

+ 1 - 1
src/vendor/bootstrap/bootstrap.js

@@ -2319,4 +2319,4 @@
   })
 
 
-}(window.jQuery);
+}(window.jQuery);

File diff suppressed because it is too large
+ 0 - 1
src/vendor/jquery/jquery-1.8.0.js


File diff suppressed because it is too large
+ 1 - 0
src/vendor/jquery/jquery-2.1.1.min.js


Some files were not shown because too many files changed in this diff