ソースを参照

Merge branch 'master' of github.com:torkelo/grafana-private into pro

Conflicts:
	src/test/test-main.js
Torkel Ödegaard 11 年 前
コミット
baf99e8650
82 ファイル変更2053 行追加954 行削除
  1. 15 0
      CHANGELOG.md
  2. 24 26
      README.md
  3. 1 1
      package.json
  4. 16 7
      src/app/app.js
  5. 44 17
      src/app/components/kbn.js
  6. 1 1
      src/app/components/lodash.extended.js
  7. 2 1
      src/app/components/require.config.js
  8. 6 2
      src/app/components/settings.js
  9. 20 0
      src/app/components/store.js
  10. 49 6
      src/app/components/timeSeries.js
  11. 2 2
      src/app/controllers/all.js
  12. 3 16
      src/app/controllers/console-ctrl.js
  13. 16 7
      src/app/controllers/dashboardCtrl.js
  14. 11 8
      src/app/controllers/dashboardNavCtrl.js
  15. 67 7
      src/app/controllers/grafanaCtrl.js
  16. 1 1
      src/app/controllers/graphiteTarget.js
  17. 33 59
      src/app/controllers/row.js
  18. 8 1
      src/app/controllers/search.js
  19. 1 1
      src/app/directives/addGraphiteFunc.js
  20. 0 35
      src/app/directives/addPanel.js
  21. 1 2
      src/app/directives/all.js
  22. 3 3
      src/app/directives/bodyClass.js
  23. 1 1
      src/app/directives/configModal.js
  24. 19 19
      src/app/directives/grafanaGraph.js
  25. 26 31
      src/app/directives/grafanaPanel.js
  26. 2 13
      src/app/directives/grafanaSimplePanel.js
  27. 1 1
      src/app/directives/grafanaVersionCheck.js
  28. 3 2
      src/app/directives/ngModelOnBlur.js
  29. 2 2
      src/app/panels/graph/legend.html
  30. 2 4
      src/app/panels/graph/module.html
  31. 25 41
      src/app/panels/graph/module.js
  32. 80 0
      src/app/panels/graph/seriesOverridesCtrl.js
  33. 46 2
      src/app/panels/graph/styleEditor.html
  34. 1 1
      src/app/panels/text/module.html
  35. 15 20
      src/app/panels/text/module.js
  36. 2 9
      src/app/panels/timepicker/module.html
  37. 29 6
      src/app/panels/timepicker/module.js
  38. 0 69
      src/app/partials/dashLoader.html
  39. 16 18
      src/app/partials/dashboard.html
  40. 77 0
      src/app/partials/dashboard_topnav.html
  41. 1 1
      src/app/partials/dasheditor.html
  42. 0 4
      src/app/partials/load.html
  43. 2 17
      src/app/partials/roweditor.html
  44. 39 43
      src/app/partials/search.html
  45. 4 8
      src/app/routes/dashboard-default.js
  46. 1 0
      src/app/routes/dashboard-from-db.js
  47. 1 0
      src/app/services/all.js
  48. 2 16
      src/app/services/dashboard/dashboardKeyBindings.js
  49. 144 38
      src/app/services/dashboard/dashboardSrv.js
  50. 156 0
      src/app/services/dashboard/dashboardViewStateSrv.js
  51. 2 0
      src/app/services/datasourceSrv.js
  52. 2 2
      src/app/services/graphite/graphiteDatasource.js
  53. 3 0
      src/app/services/graphite/lexer.js
  54. 32 58
      src/app/services/panelSrv.js
  55. 6 4
      src/app/services/playlistSrv.js
  56. 6 0
      src/app/services/unsavedChangesSrv.js
  57. 5 1
      src/config.sample.js
  58. 0 8
      src/css/bootstrap.dark.min.css
  59. 0 8
      src/css/bootstrap.light.min.css
  60. 44 12
      src/css/less/grafana.less
  61. 17 1
      src/css/less/graph.less
  62. 9 1
      src/css/less/variables.dark.less
  63. 10 1
      src/css/less/variables.light.less
  64. 1 1
      src/index.html
  65. 55 0
      src/plugins/datasource.example.js
  66. 71 4
      src/test/specs/dashboardSrv-specs.js
  67. 37 0
      src/test/specs/dashboardViewStateSrv-specs.js
  68. 147 0
      src/test/specs/grafanaGraph-specs.js
  69. 9 2
      src/test/specs/helpers.js
  70. 18 0
      src/test/specs/kbn-format-specs.js
  71. 11 4
      src/test/specs/lexer-specs.js
  72. 2 2
      src/test/specs/parser-specs.js
  73. 0 44
      src/test/specs/row-ctrl-specs.js
  74. 51 0
      src/test/specs/seriesOverridesCtrl-specs.js
  75. 115 0
      src/test/specs/timeSeries-specs.js
  76. 6 1
      src/test/test-main.js
  77. 8 3
      src/vendor/angular/angular-dragdrop.js
  78. 2 1
      src/vendor/angular/angular-strap.js
  79. 3 2
      src/vendor/bootstrap/bootstrap.js
  80. 0 1
      src/vendor/jquery/jquery-1.8.0.js
  81. 1 0
      src/vendor/jquery/jquery-2.1.1.min.js
  82. 359 224
      src/vendor/moment.js

+ 15 - 0
CHANGELOG.md

@@ -3,12 +3,27 @@
 **New features and improvements**
 **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 #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.
+- [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
+- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
+
+**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)
+- [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
+- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected. Closes #277
 
 
 **Tech**
 **Tech**
 - Upgraded from angularjs 1.1.5 to 1.3 beta 17;
 - Upgraded from angularjs 1.1.5 to 1.3 beta 17;
 - Switch from underscore to lodash
 - Switch from underscore to lodash
 - helpers to easily unit test angularjs controllers and services
 - helpers to easily unit test angularjs controllers and services
 - Test coverage through coveralls
 - 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)
 # 1.7.0 (2014-08-11)
 
 

+ 24 - 26
README.md

@@ -38,7 +38,7 @@ Graphite, InfluxDB & OpenTSDB.
 - Import dashboard from Graphite
 - Import dashboard from Graphite
 - Templating
 - Templating
 - [Scripted dashboards](http://grafana.org/docs/features/scripted_dashboards)
 - [Scripted dashboards](http://grafana.org/docs/features/scripted_dashboards)
-- [Dashboard playlists](http://grafana.org/docs/docs/features/playlist)
+- [Dashboard playlists](http://grafana.org/docs/features/playlist)
 - [Time range controls](http://grafana.org/docs/features/time_range)
 - [Time range controls](http://grafana.org/docs/features/time_range)
 
 
 ### InfluxDB
 ### InfluxDB
@@ -49,40 +49,38 @@ Graphite, InfluxDB & OpenTSDB.
 - Use as metric data source
 - Use as metric data source
 - Query editor with metric name typeahead and tag filtering
 - Query editor with metric name typeahead and tag filtering
 
 
-# Requirements
+## Requirements
 There are no dependencies, Grafana is a client side application that runs in your browser. It only needs a time series store where it can fetch metrics. If you use InfluxDB Grafana can use it to store dashboards. If you use Graphite or OpenTSDB you can use Elasticsearch to store dashboards or just use json files stored on disk.
 There are no dependencies, Grafana is a client side application that runs in your browser. It only needs a time series store where it can fetch metrics. If you use InfluxDB Grafana can use it to store dashboards. If you use Graphite or OpenTSDB you can use Elasticsearch to store dashboards or just use json files stored on disk.
 
 
-# Installation
+## Installation
 Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/)
 Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/)
 the latest release.
 the latest release.
 
 
 Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please
 Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please
 read the [troubleshooting guide](http://grafana.org/docs/troubleshooting).
 read the [troubleshooting guide](http://grafana.org/docs/troubleshooting).
 
 
-# Documentation & Support
+## Documentation & Support
 Be sure to read the [getting started guide](http://grafana.org/docs/features/intro) and the other
 Be sure to read the [getting started guide](http://grafana.org/docs/features/intro) and the other
 feature guides.
 feature guides.
 
 
-# Roadmap
-- Improve graphite query editor to handle all types of queries
-- Refine and simplify common tasks
-- More panel types (not just graphs)
-- Improve templating support
-- Alerting
-- Optional backend component
-- Much much more! (what ever gets votes on github issues!)
-
-# Contribute
-If you have any idea for an improvement or found a bug do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about!
-
-Clone repository:
-- npm install
-- grunt server (starts development web server in src folder)
-- grunt (runs jshint and less -> css compilation)
-- npm test runs jshint, and unit tests
-
-Before submitting a PR be sure that there are no jshint or unit test failures.
-And [sign the CLA](http://grafana.org/docs/contributing/cla.html)
-
-# License
+## Run from master
+Grafana uses nodejs and grunt for asset management (css & javascript), unit test runner and javascript syntax verification.
+- clone repository
+- install nodejs
+- npm install (in project root)
+- npm install -g grunt-cli
+- grunt   (runt default task that will generate css files)
+- grunt build (creates optimized & minified release)
+- grunt release (same as grunt build but will also create tar & zip package)
+- grunt test (executes jshint and unit tests)
+
+## Contribute
+If you have any idea for an improvement or found a bug do not hesitate to open an issue.
+And if you have time clone this repo and submit a pull request and help me make Grafana
+the kickass metrics & devops dashboard we all dream about!
+
+Before creating a pull request be sure that "grunt test" runs without any style or unit test errors, also
+please [sign the CLA](http://grafana.org/docs/contributing/cla.html)
+
+## License
 Grafana is distributed under Apache 2.0 License.
 Grafana is distributed under Apache 2.0 License.

+ 1 - 1
package.json

@@ -34,7 +34,7 @@
     "grunt-string-replace": "~0.2.4",
     "grunt-string-replace": "~0.2.4",
     "grunt-usemin": "^2.1.1",
     "grunt-usemin": "^2.1.1",
     "jshint-stylish": "~0.1.5",
     "jshint-stylish": "~0.1.5",
-    "karma": "~0.12.16",
+    "karma": "~0.12.21",
     "karma-chrome-launcher": "~0.1.4",
     "karma-chrome-launcher": "~0.1.4",
     "karma-coffee-preprocessor": "~0.1.2",
     "karma-coffee-preprocessor": "~0.1.2",
     "karma-coverage": "^0.2.5",
     "karma-coverage": "^0.2.5",

+ 16 - 7
src/app/app.js

@@ -78,14 +78,23 @@ function (angular, $, _, appLevelRequire, config) {
     apps_deps.push(module_name);
     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() {
   app.boot = function() {
-    require([
-      'controllers/all',
-      'directives/all',
-      'filters/all',
-      'components/partials',
-      'routes/all',
-    ], function () {
+    require(preBootRequires, function () {
+
+      // disable tool tip animation
+      $.fn.tooltip.defaults.animation = false;
 
 
       // bootstrap the app
       // bootstrap the app
       angular
       angular

+ 44 - 17
src/app/components/kbn.js

@@ -1,4 +1,8 @@
-define(['jquery','lodash','moment'],
+define([
+  'jquery',
+  'lodash',
+  'moment'
+],
 function($, _, moment) {
 function($, _, moment) {
   'use strict';
   'use strict';
 
 
@@ -227,36 +231,36 @@ function($, _, moment) {
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
           roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('years',num);
+          dateTime.add(num, 'years');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('years',num);
+          dateTime.subtract(num, 'years');
         }
         }
         break;
         break;
       case 'M':
       case 'M':
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
           roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('months',num);
+          dateTime.add(num, 'months');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('months',num);
+          dateTime.subtract(num, 'months');
         }
         }
         break;
         break;
       case 'w':
       case 'w':
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
           roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('weeks',num);
+          dateTime.add(num, 'weeks');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('weeks',num);
+          dateTime.subtract(num, 'weeks');
         }
         }
         break;
         break;
       case 'd':
       case 'd':
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
           roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('days',num);
+          dateTime.add(num, 'days');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('days',num);
+          dateTime.subtract(num, 'days');
         }
         }
         break;
         break;
       case 'h':
       case 'h':
@@ -264,27 +268,27 @@ function($, _, moment) {
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
           roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('hours',num);
+          dateTime.add(num, 'hours');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('hours',num);
+          dateTime.subtract(num,'hours');
         }
         }
         break;
         break;
       case 'm':
       case 'm':
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
           roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('minutes',num);
+          dateTime.add(num, 'minutes');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('minutes',num);
+          dateTime.subtract(num, 'minutes');
         }
         }
         break;
         break;
       case 's':
       case 's':
         if (type === 0) {
         if (type === 0) {
           roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
           roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
         } else if (type === 1) {
         } else if (type === 1) {
-          dateTime.add('seconds',num);
+          dateTime.add(num, 'seconds');
         } else if (type === 2) {
         } else if (type === 2) {
-          dateTime.subtract('seconds',num);
+          dateTime.subtract(num, 'seconds');
         }
         }
         break;
         break;
       default:
       default:
@@ -525,12 +529,35 @@ function($, _, moment) {
         return kbn.nanosFormat(val, decimals);
         return kbn.nanosFormat(val, decimals);
       };
       };
     default:
     default:
-      return function(val) {
-        return val % 1 === 0 ? val : val.toFixed(decimals);
+      return function(val, axis) {
+        return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
       };
       };
     }
     }
   };
   };
 
 
+  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 || value === 0) {
+      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) {
   kbn.msFormat = function(size, decimals) {
     // Less than 1 milli, downscale to micro
     // Less than 1 milli, downscale to micro
     if (Math.abs(size) < 1) {
     if (Math.abs(size) < 1) {

+ 1 - 1
src/app/components/lodash.extended.js

@@ -33,4 +33,4 @@ function () {
   });
   });
 
 
   return _;
   return _;
-});
+});

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

@@ -8,6 +8,7 @@ require.config({
     config:                   ['../config', '../config.sample'],
     config:                   ['../config', '../config.sample'],
     settings:                 'components/settings',
     settings:                 'components/settings',
     kbn:                      'components/kbn',
     kbn:                      'components/kbn',
+    store:                    'components/store',
 
 
     css:                      '../vendor/require/css',
     css:                      '../vendor/require/css',
     text:                     '../vendor/require/text',
     text:                     '../vendor/require/text',
@@ -27,7 +28,7 @@ require.config({
     'lodash-src':             '../vendor/lodash',
     'lodash-src':             '../vendor/lodash',
     bootstrap:                '../vendor/bootstrap/bootstrap',
     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',
     'jquery-ui':              '../vendor/jquery/jquery-ui-1.10.3',
 
 
     'extend-jquery':          'components/extend-jquery',
     'extend-jquery':          'components/extend-jquery',

+ 6 - 2
src/app/components/settings.js

@@ -19,7 +19,7 @@ function (_, crypto) {
       default_route                 : '/dashboard/file/default.json',
       default_route                 : '/dashboard/file/default.json',
       playlist_timespan             : "1m",
       playlist_timespan             : "1m",
       unsaved_changes_warning       : true,
       unsaved_changes_warning       : true,
-      search                        : { max_results: 20 },
+      search                        : { max_results: 16 },
       admin                         : {}
       admin                         : {}
     };
     };
 
 
@@ -70,7 +70,7 @@ function (_, crypto) {
 
 
     _.each(settings.datasources, function(datasource, key) {
     _.each(settings.datasources, function(datasource, key) {
       datasource.name = key;
       datasource.name = key;
-      parseBasicAuth(datasource);
+      if (datasource.url) { parseBasicAuth(datasource); }
       if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
       if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
     });
     });
 
 
@@ -78,6 +78,10 @@ function (_, crypto) {
       settings.panels = _.union(settings.panels, settings.plugins.panels);
       settings.panels = _.union(settings.panels, settings.plugins.panels);
     }
     }
 
 
+    if (!settings.plugins.dependencies) {
+      settings.plugins.dependencies = [];
+    }
+
     return settings;
     return settings;
   };
   };
 });
 });

+ 20 - 0
src/app/components/store.js

@@ -0,0 +1,20 @@
+define([], function() {
+  'use strict';
+
+  return {
+    get: function(key) {
+      return window.localStorage[key];
+    },
+    set: function(key, value) {
+      window.localStorage[key] = value;
+    },
+    getBool: function(key) {
+      return window.localStorage[key] === 'true' ? true : false;
+    },
+    delete: function(key) {
+      window.localStorage.removeItem(key);
+    }
+
+  };
+
+});

+ 49 - 6
src/app/panels/graph/timeSeries.js → src/app/components/timeSeries.js

@@ -5,15 +5,57 @@ define([
 function (_, kbn) {
 function (_, kbn) {
   'use strict';
   'use strict';
 
 
-  var ts = {};
-
-  ts.ZeroFilled = function (opts) {
+  function TimeSeries(opts) {
     this.datapoints = opts.datapoints;
     this.datapoints = opts.datapoints;
     this.info = opts.info;
     this.info = opts.info;
     this.label = opts.info.alias;
     this.label = opts.info.alias;
+  }
+
+  function matchSeriesOverride(aliasOrRegex, seriesAlias) {
+    if (!aliasOrRegex) { return false; }
+
+    if (aliasOrRegex[0] === '/') {
+      var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+      var regex = new RegExp(match[1], match[2]);
+      return seriesAlias.match(regex) != null;
+    }
+
+    return aliasOrRegex === seriesAlias;
+  }
+
+  function translateFillOption(fill) {
+    return fill === 0 ? 0.001 : fill/10;
+  }
+
+  TimeSeries.prototype.applySeriesOverrides = function(overrides) {
+    this.lines = {};
+    this.points = {};
+    this.bars = {};
+    this.info.yaxis = 1;
+    this.zindex = 0;
+    delete this.stack;
+
+    for (var i = 0; i < overrides.length; i++) {
+      var override = overrides[i];
+      if (!matchSeriesOverride(override.alias, this.info.alias)) {
+        continue;
+      }
+      if (override.lines !== void 0) { this.lines.show = override.lines; }
+      if (override.points !== void 0) { this.points.show = override.points; }
+      if (override.bars !== void 0) { this.bars.show = override.bars; }
+      if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
+      if (override.stack !== void 0) { this.stack = override.stack; }
+      if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; }
+      if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
+      if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
+      if (override.zindex !== void 0) { this.zindex = override.zindex; }
+      if (override.yaxis !== void 0) {
+        this.info.yaxis = override.yaxis;
+      }
+    }
   };
   };
 
 
-  ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) {
+  TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
     var result = [];
     var result = [];
 
 
     this.color = this.info.color;
     this.color = this.info.color;
@@ -74,5 +116,6 @@ function (_, kbn) {
     return result;
     return result;
   };
   };
 
 
-  return ts;
-});
+  return TimeSeries;
+
+});

+ 2 - 2
src/app/controllers/all.js

@@ -1,7 +1,7 @@
 define([
 define([
   './grafanaCtrl',
   './grafanaCtrl',
-  './dash',
-  './dashLoader',
+  './dashboardCtrl',
+  './dashboardNavCtrl',
   './row',
   './row',
   './submenuCtrl',
   './submenuCtrl',
   './pulldown',
   './pulldown',

+ 3 - 16
src/app/controllers/console-ctrl.js

@@ -2,12 +2,13 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'moment',
   'moment',
+  'store'
 ],
 ],
-function (angular, _, moment) {
+function (angular, _, moment, store) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
-  var consoleEnabled = window.localStorage && window.localStorage.grafanaConsole === 'true';
+  var consoleEnabled = store.getBool('grafanaConsole');
 
 
   if (!consoleEnabled) {
   if (!consoleEnabled) {
     return;
     return;
@@ -15,20 +16,6 @@ function (angular, _, moment) {
 
 
   var events = [];
   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) {
   function ConsoleEvent(type, title, data) {
     this.type = type;
     this.type = type;
     this.title = title;
     this.title = title;

+ 16 - 7
src/app/controllers/dash.js → src/app/controllers/dashboardCtrl.js

@@ -10,8 +10,10 @@ function (angular, $, config, _) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('DashCtrl', function(
-    $scope, $rootScope, dashboardKeybindings, filterSrv, dashboardSrv, panelMoveSrv, timer) {
+  module.controller('DashboardCtrl', function(
+      $scope, $rootScope, dashboardKeybindings,
+      filterSrv, dashboardSrv, dashboardViewStateSrv,
+      panelMoveSrv, timer, $timeout) {
 
 
     $scope.editor = { index: 0 };
     $scope.editor = { index: 0 };
     $scope.panelNames = config.panels;
     $scope.panelNames = config.panels;
@@ -19,14 +21,25 @@ function (angular, $, config, _) {
     $scope.init = function() {
     $scope.init = function() {
       $scope.availablePanels = config.panels;
       $scope.availablePanels = config.panels;
       $scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
       $scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
+
+      angular.element(window).bind('resize', function() {
+        $timeout(function() {
+          $scope.$broadcast('render');
+        });
+      });
+
     };
     };
 
 
     $scope.setupDashboard = function(event, dashboardData) {
     $scope.setupDashboard = function(event, dashboardData) {
       timer.cancel_all();
       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.dashboard = dashboardSrv.create(dashboardData);
+      $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
+
       $scope.grafana.style = $scope.dashboard.style;
       $scope.grafana.style = $scope.dashboard.style;
 
 
       $scope.filter = filterSrv;
       $scope.filter = filterSrv;
@@ -78,10 +91,6 @@ function (angular, $, config, _) {
       };
       };
     };
     };
 
 
-    $scope.row_style = function(row) {
-      return { 'min-height': row.collapse ? '5px' : row.height };
-    };
-
     $scope.panel_path =function(type) {
     $scope.panel_path =function(type) {
       if(type) {
       if(type) {
         return 'app/panels/'+type.replace(".","/");
         return 'app/panels/'+type.replace(".","/");

+ 11 - 8
src/app/controllers/dashLoader.js → src/app/controllers/dashboardNavCtrl.js

@@ -3,17 +3,19 @@ define([
   'lodash',
   'lodash',
   'moment',
   'moment',
   'config',
   'config',
+  'store',
   'filesaver'
   'filesaver'
 ],
 ],
-function (angular, _, moment, config) {
+function (angular, _, moment, config, store) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('dashLoader', function($scope, $rootScope, $http, alertSrv, $location, playlistSrv, datasourceSrv) {
+  module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.db = datasourceSrv.getGrafanaDB();
       $scope.db = datasourceSrv.getGrafanaDB();
+
       $scope.onAppEvent('save-dashboard', function() {
       $scope.onAppEvent('save-dashboard', function() {
         $scope.saveDashboard();
         $scope.saveDashboard();
       });
       });
@@ -21,19 +23,16 @@ function (angular, _, moment, config) {
       $scope.onAppEvent('zoom-out', function() {
       $scope.onAppEvent('zoom-out', function() {
         $scope.zoom(2);
         $scope.zoom(2);
       });
       });
-    };
 
 
-    $scope.exitFullscreen = function() {
-      $scope.emitAppEvent('panel-fullscreen-exit');
     };
     };
 
 
     $scope.set_default = function() {
     $scope.set_default = function() {
-      window.localStorage.grafanaDashboardDefault = $location.path();
+      store.set('grafanaDashboardDefault', $location.path());
       alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
       alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
     };
     };
 
 
     $scope.purge_default = function() {
     $scope.purge_default = function() {
-      delete window.localStorage.grafanaDashboardDefault;
+      store.delete('grafanaDashboardDefault');
       alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default','success', 5000);
       alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default','success', 5000);
     };
     };
 
 
@@ -78,6 +77,7 @@ function (angular, _, moment, config) {
         .then(function(result) {
         .then(function(result) {
           alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
           alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
 
 
+          $location.search({});
           $location.path(result.url);
           $location.path(result.url);
 
 
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
@@ -87,7 +87,9 @@ function (angular, _, moment, config) {
         });
         });
     };
     };
 
 
-    $scope.deleteDashboard = function(id) {
+    $scope.deleteDashboard = function(id, $event) {
+      $event.stopPropagation();
+
       if (!confirm('Are you sure you want to delete dashboard?')) {
       if (!confirm('Are you sure you want to delete dashboard?')) {
         return;
         return;
       }
       }
@@ -135,6 +137,7 @@ function (angular, _, moment, config) {
 
 
     $scope.openSaveDropdown = function() {
     $scope.openSaveDropdown = function() {
       $scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
       $scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
+      $scope.saveDropdownOpened = true;
     };
     };
 
 
     $scope.markAsFavorite = function() {
     $scope.markAsFavorite = function() {

+ 67 - 7
src/app/controllers/grafanaCtrl.js

@@ -2,8 +2,10 @@ define([
   'angular',
   'angular',
   'config',
   'config',
   'lodash',
   'lodash',
+  'jquery',
+  'store',
 ],
 ],
-function (angular, config, _) {
+function (angular, config, _, $, store) {
   "use strict";
   "use strict";
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
@@ -11,20 +13,23 @@ function (angular, config, _) {
   module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
   module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
 
 
     $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
     $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
+    $scope.consoleEnabled = store.getBool('grafanaConsole');
+
+    $rootScope.profilingEnabled = store.getBool('profilingEnabled');
+    $rootScope.performance = { loadStart: new Date().getTime() };
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope._ = _;
       $scope._ = _;
-      $scope.dashAlerts = alertSrv;
-      $scope.grafana = {
-        style: 'dark'
-      };
 
 
-      $scope.consoleEnabled = (window.localStorage && window.localStorage.grafanaConsole === 'true');
+      if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
+
+      $scope.dashAlerts = alertSrv;
+      $scope.grafana = { style: 'dark' };
     };
     };
 
 
     $scope.toggleConsole = function() {
     $scope.toggleConsole = function() {
       $scope.consoleEnabled = !$scope.consoleEnabled;
       $scope.consoleEnabled = !$scope.consoleEnabled;
-      window.localStorage.grafanaConsole = $scope.consoleEnabled ? 'true' : 'false';
+      store.set('grafanaConsole', $scope.consoleEnabled);
     };
     };
 
 
     $rootScope.onAppEvent = function(name, callback) {
     $rootScope.onAppEvent = function(name, callback) {
@@ -46,6 +51,61 @@ function (angular, config, _) {
       "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"  //7
       "#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();
     $scope.init();
 
 
   });
   });

+ 1 - 1
src/app/controllers/graphiteTarget.js

@@ -289,7 +289,7 @@ function (angular, _, config, gfunc, Parser) {
       this.expandable = options.expandable;
       this.expandable = options.expandable;
 
 
       if (options.type === 'template') {
       if (options.type === 'template') {
-        this.html = $sce.trustAsHtml("<span style='color: #ECEC09'>" + options.value + "</span>");
+        this.html = $sce.trustAsHtml(options.value);
       }
       }
       else {
       else {
         this.html = $sce.trustAsHtml(this.value);
         this.html = $sce.trustAsHtml(this.value);

+ 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
     // This can be overridden by individual panels
     $scope.close_edit = function() {
     $scope.close_edit = function() {
       $scope.$broadcast('render');
       $scope.$broadcast('render');
     };
     };
 
 
     $scope.add_panel = function(panel) {
     $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() {
     $scope.delete_row = function() {
@@ -100,45 +77,17 @@ function (angular, app, _) {
     };
     };
 
 
     $scope.duplicatePanel = function(panel, row) {
     $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) {
     $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 = {
       $scope.panel = {
         error   : false,
         error   : false,
-        /** @scratch /panels/1
-         * span:: A number, 1-12, that describes the width of the panel.
-         */
         span    : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
         span    : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
-        /** @scratch /panels/1
-         * editable:: Enable or disable the edit button the the panel
-         */
         editable: true,
         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
         type    : type
       };
       };
 
 
@@ -155,12 +104,37 @@ function (angular, app, _) {
       $scope.row.height = fixRowHeight($scope.row.height);
       $scope.row.height = fixRowHeight($scope.row.height);
     };
     };
 
 
-    /** @scratch /panels/2
-     * --
-     */
-
     $scope.init();
     $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();
+        }
+      });
+    };
+  });
+
 });
 });

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

@@ -41,6 +41,7 @@ function (angular, _, config, $) {
 
 
         var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
         var selectedDash = $scope.results.dashboards[$scope.selectedIndex];
         if (selectedDash) {
         if (selectedDash) {
+          $location.search({});
           $location.path("/dashboard/db/" + selectedDash.id);
           $location.path("/dashboard/db/" + selectedDash.id);
           setTimeout(function() {
           setTimeout(function() {
             $('body').click(); // hack to force dropdown to close;
             $('body').click(); // hack to force dropdown to close;
@@ -49,7 +50,12 @@ function (angular, _, config, $) {
       }
       }
     };
     };
 
 
-    $scope.shareDashboard = function(title, id) {
+    $scope.goToDashboard = function(id) {
+      $location.path("/dashboard/db/" + id);
+    };
+
+    $scope.shareDashboard = function(title, id, $event) {
+      $event.stopPropagation();
       var baseUrl = window.location.href.replace(window.location.hash,'');
       var baseUrl = window.location.href.replace(window.location.hash,'');
 
 
       $scope.share = {
       $scope.share = {
@@ -98,6 +104,7 @@ function (angular, _, config, $) {
         $element.next().find('.dropdown-toggle').dropdown('toggle');
         $element.next().find('.dropdown-toggle').dropdown('toggle');
       }
       }
 
 
+      $scope.searchOpened = true;
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
       $scope.query.query = 'title:';
       $scope.query.query = 'title:';
       $scope.search();
       $scope.search();

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

@@ -97,4 +97,4 @@ function (angular, app, _, $, gfunc) {
       };
       };
     });
     });
   }
   }
-});
+});

+ 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([
 define([
-  './addPanel',
   './arrayJoin',
   './arrayJoin',
   './dashUpload',
   './dashUpload',
   './grafanaPanel',
   './grafanaPanel',
@@ -17,4 +16,4 @@ define([
   './graphiteFuncEditor',
   './graphiteFuncEditor',
   './grafanaVersionCheck',
   './grafanaVersionCheck',
   './influxdbFuncEditor'
   './influxdbFuncEditor'
-], function () {});
+], function () {});

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

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

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

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

+ 19 - 19
src/app/directives/grafanaGraph.js

@@ -21,7 +21,6 @@ function (angular, $, kbn, moment, _) {
         var legendSideLastValue = null;
         var legendSideLastValue = null;
 
 
         scope.$on('refresh',function() {
         scope.$on('refresh',function() {
-          if (scope.otherPanelInFullscreenMode()) { return; }
           scope.get_data();
           scope.get_data();
         });
         });
 
 
@@ -39,15 +38,14 @@ function (angular, $, kbn, moment, _) {
         // Receive render events
         // Receive render events
         scope.$on('render',function(event, renderData) {
         scope.$on('render',function(event, renderData) {
           data = renderData || data;
           data = renderData || data;
+          if (!data) {
+            scope.get_data();
+            return;
+          }
           annotations = data.annotations || annotations;
           annotations = data.annotations || annotations;
           render_panel();
           render_panel();
         });
         });
 
 
-        // Re-render if the window is resized
-        angular.element(window).bind('resize', function() {
-          render_panel();
-        });
-
         function setElementHeight() {
         function setElementHeight() {
           try {
           try {
             var height = scope.height || scope.panel.height || scope.row.height;
             var height = scope.height || scope.panel.height || scope.row.height;
@@ -115,7 +113,7 @@ function (angular, $, kbn, moment, _) {
               lines:  {
               lines:  {
                 show: panel.lines,
                 show: panel.lines,
                 zero: false,
                 zero: false,
-                fill: panel.fill === 0 ? 0.001 : panel.fill/10,
+                fill: translateFillOption(panel.fill),
                 lineWidth: panel.linewidth,
                 lineWidth: panel.linewidth,
                 steps: panel.steppedLine
                 steps: panel.steppedLine
               },
               },
@@ -151,11 +149,12 @@ function (angular, $, kbn, moment, _) {
           };
           };
 
 
           for (var i = 0; i < data.length; i++) {
           for (var i = 0; i < data.length; i++) {
-            var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats);
-            data[i].data = _d;
+            var series = data[i];
+            series.applySeriesOverrides(panel.seriesOverrides);
+            series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
           }
           }
 
 
-          if (panel.bars && data.length && data[0].info.timeStep) {
+          if (data.length && data[0].info.timeStep) {
             options.series.bars.barWidth = data[0].info.timeStep / 1.5;
             options.series.bars.barWidth = data[0].info.timeStep / 1.5;
           }
           }
 
 
@@ -164,21 +163,27 @@ function (angular, $, kbn, moment, _) {
           addAnnotations(options);
           addAnnotations(options);
           configureAxisOptions(data, options);
           configureAxisOptions(data, options);
 
 
+          var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
+
           // if legend is to the right delay plot draw a few milliseconds
           // if legend is to the right delay plot draw a few milliseconds
           // so the legend width calculation can be done
           // so the legend width calculation can be done
           if (shouldDelayDraw(panel)) {
           if (shouldDelayDraw(panel)) {
             legendSideLastValue = panel.legend.rightSide;
             legendSideLastValue = panel.legend.rightSide;
             setTimeout(function() {
             setTimeout(function() {
-              plot = $.plot(elem, data, options);
+              plot = $.plot(elem, sortedSeries, options);
               addAxisLabels();
               addAxisLabels();
             }, 50);
             }, 50);
           }
           }
           else {
           else {
-            plot = $.plot(elem, data, options);
+            plot = $.plot(elem, sortedSeries, options);
             addAxisLabels();
             addAxisLabels();
           }
           }
         }
         }
 
 
+        function translateFillOption(fill) {
+          return fill === 0 ? 0.001 : fill/10;
+        }
+
         function shouldDelayDraw(panel) {
         function shouldDelayDraw(panel) {
           if (panel.legend.rightSide) {
           if (panel.legend.rightSide) {
             return true;
             return true;
@@ -300,9 +305,7 @@ function (angular, $, kbn, moment, _) {
         }
         }
 
 
         function configureAxisMode(axis, format) {
         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) {
         function time_format(interval, ticks, min, max) {
@@ -353,10 +356,7 @@ function (angular, $, kbn, moment, _) {
             }
             }
 
 
             value = kbn.getFormatFunction(format, 2)(value);
             value = kbn.getFormatFunction(format, 2)(value);
-
-            timestamp = dashboard.timezone === 'browser' ?
-              moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
-              moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
+            timestamp = dashboard.formatDate(item.datapoint[0]);
 
 
             $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
             $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
           } else {
           } else {

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

@@ -8,7 +8,7 @@ function (angular, $) {
 
 
   angular
   angular
     .module('grafana.directives')
     .module('grafana.directives')
-    .directive('grafanaPanel', function($compile) {
+    .directive('grafanaPanel', function($compile, $parse) {
 
 
       var container = '<div class="panel-container"></div>';
       var container = '<div class="panel-container"></div>';
       var content = '<div class="panel-content"></div>';
       var content = '<div class="panel-content"></div>';
@@ -18,11 +18,13 @@ function (angular, $) {
        '<div class="row-fluid panel-extra">' +
        '<div class="row-fluid panel-extra">' +
           '<div class="panel-extra-container">' +
           '<div class="panel-extra-container">' +
             '<span class="alert-error panel-error small pointer"' +
             '<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>' +
               '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
+              '</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>' +
               '<i class="icon-spinner icon-spin icon-large"></i>' +
             '</span>' +
             '</span>' +
 
 
@@ -36,7 +38,7 @@ function (angular, $) {
                 'index:{{$index}},'+
                 'index:{{$index}},'+
                 'onStart:\'panelMoveStart\','+
                 'onStart:\'panelMoveStart\','+
                 'onStop:\'panelMoveStop\''+
                 'onStop:\'panelMoveStop\''+
-                '}"  ng-model="row.panels" ' +
+                '}"  ng-model="panel" ' +
                 '>' +
                 '>' +
                 '{{panel.title || "No title"}}' +
                 '{{panel.title || "No title"}}' +
               '</span>' +
               '</span>' +
@@ -49,8 +51,7 @@ function (angular, $) {
       return {
       return {
         restrict: 'E',
         restrict: 'E',
         link: function($scope, elem, attr) {
         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();
           var newScope = $scope.$new();
 
 
           $scope.kbnJqUiDraggableOptions = {
           $scope.kbnJqUiDraggableOptions = {
@@ -68,6 +69,14 @@ function (angular, $) {
             /* jshint indent:false */
             /* jshint indent:false */
             $compile(elem.contents())(newScope);
             $compile(elem.contents())(newScope);
             elem.removeClass("ng-cloak");
             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() {
           newScope.$on('$destroy',function() {
@@ -75,31 +84,17 @@ function (angular, $) {
             elem.remove();
             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([
 define([
   'angular',
   'angular',
-  'lodash'
 ],
 ],
-function (angular, _) {
+function (angular) {
   'use strict';
   'use strict';
 
 
   angular
   angular
@@ -60,18 +59,8 @@ function (angular, _) {
             loadController(name);
             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;
             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) {
             .then(function(response) {
               if (!response.data || !response.data.version) {
               if (!response.data || !response.data.version) {
                 return;
                 return;

+ 3 - 2
src/app/directives/ngModelOnBlur.js

@@ -7,13 +7,14 @@ function (angular) {
     .directive('ngModelOnblur', function() {
     .directive('ngModelOnblur', function() {
       return {
       return {
         restrict: 'A',
         restrict: 'A',
+        priority: 1,
         require: 'ngModel',
         require: 'ngModel',
         link: function(scope, elm, attr, ngModelCtrl) {
         link: function(scope, elm, attr, ngModelCtrl) {
           if (attr.type === 'radio' || attr.type === 'checkbox') {
           if (attr.type === 'radio' || attr.type === 'checkbox') {
             return;
             return;
           }
           }
 
 
-          elm.unbind('input').unbind('keydown').unbind('change');
+          elm.off('input keydown change');
           elm.bind('blur', function() {
           elm.bind('blur', function() {
             scope.$apply(function() {
             scope.$apply(function() {
               ngModelCtrl.$setViewValue(elm.val());
               ngModelCtrl.$setViewValue(elm.val());
@@ -22,4 +23,4 @@ function (angular) {
         }
         }
       };
       };
     });
     });
-});
+});

+ 2 - 2
src/app/panels/graph/legend.html

@@ -34,12 +34,12 @@
 
 
     <div class="editor-row small" style="padding-bottom: 0;">
     <div class="editor-row small" style="padding-bottom: 0;">
       <label>Axis:</label>
       <label>Axis:</label>
-      <button ng-click="toggleYAxis(series)"
+      <button ng-click="toggleYAxis(series);dismiss();"
               class="btn btn-mini"
               class="btn btn-mini"
               ng-class="{'btn-success': series.yaxis === 1 }">
               ng-class="{'btn-success': series.yaxis === 1 }">
         Left
         Left
       </button>
       </button>
-      <button ng-click="toggleYAxis(series)"
+      <button ng-click="toggleYAxis(series);dismiss();"
               class="btn btn-mini"
               class="btn btn-mini"
               ng-class="{'btn-success': series.yaxis === 2 }">
               ng-class="{'btn-success': series.yaxis === 2 }">
         Right
         Right

+ 2 - 4
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-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
       <div class="graph-canvas-wrapper">
       <div class="graph-canvas-wrapper">
@@ -29,7 +27,7 @@
       </div>
       </div>
     </div>
     </div>
 
 
-    <div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-show="editorTabs[editor.index] == tab.title">
+    <div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
       <div ng-include src="tab.src"></div>
       <div ng-include src="tab.src"></div>
     </div>
     </div>
   </div>
   </div>

+ 25 - 41
src/app/panels/graph/module.js

@@ -1,16 +1,3 @@
-/** @scratch /panels/5
- * include::panels/histogram.asciidoc[]
- */
-
-/** @scratch /panels/histogram/0
- * == Histogram
- * Status: *Stable*
- *
- * The histogram panel allow for the display of time charts. It includes several modes and tranformations
- * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter
- * fields.
- *
- */
 define([
 define([
   'angular',
   'angular',
   'app',
   'app',
@@ -18,7 +5,8 @@ define([
   'lodash',
   'lodash',
   'kbn',
   'kbn',
   'moment',
   'moment',
-  './timeSeries',
+  'components/timeSeries',
+  './seriesOverridesCtrl',
   'services/panelSrv',
   'services/panelSrv',
   'services/annotationsSrv',
   'services/annotationsSrv',
   'services/datasourceSrv',
   'services/datasourceSrv',
@@ -29,11 +17,10 @@ define([
   'jquery.flot.stack',
   'jquery.flot.stack',
   'jquery.flot.stackpercent'
   'jquery.flot.stackpercent'
 ],
 ],
-function (angular, app, $, _, kbn, moment, timeSeries) {
-
+function (angular, app, $, _, kbn, moment, TimeSeries) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.panels.graph', []);
+  var module = angular.module('grafana.panels.graph');
   app.useModule(module);
   app.useModule(module);
 
 
   module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) {
   module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) {
@@ -179,7 +166,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       targets: [{}],
       targets: [{}],
 
 
       aliasColors: {},
       aliasColors: {},
-      aliasYAxis: {},
+
+      seriesOverrides: [],
     };
     };
 
 
     _.defaults($scope.panel,_d);
     _.defaults($scope.panel,_d);
@@ -188,13 +176,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
     _.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.updateTimeRange = function () {
       $scope.range = $scope.filter.timeRange();
       $scope.range = $scope.filter.timeRange();
@@ -210,10 +192,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     };
     };
 
 
     $scope.get_data = function() {
     $scope.get_data = function() {
-      delete $scope.panel.error;
-
-      $scope.panelMeta.loading = true;
-
       $scope.updateTimeRange();
       $scope.updateTimeRange();
 
 
       var metricsQuery = {
       var metricsQuery = {
@@ -253,7 +231,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
 
 
       var data = _.map(results.data, $scope.seriesHandler);
       var data = _.map(results.data, $scope.seriesHandler);
 
 
-      $scope.datapointsWarning = $scope.datapointsCount || !$scope.datapointsOutside;
+      $scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
 
 
       $scope.annotationsPromise
       $scope.annotationsPromise
         .then(function(annotations) {
         .then(function(annotations) {
@@ -268,18 +246,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       var datapoints = seriesData.datapoints;
       var datapoints = seriesData.datapoints;
       var alias = seriesData.target;
       var alias = seriesData.target;
       var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
       var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
-      var yaxis = $scope.panel.aliasYAxis[alias] || 1;
 
 
       var seriesInfo = {
       var seriesInfo = {
         alias: alias,
         alias: alias,
         color:  color,
         color:  color,
-        enable: true,
-        yaxis: yaxis
       };
       };
 
 
       $scope.legend.push(seriesInfo);
       $scope.legend.push(seriesInfo);
 
 
-      var series = new timeSeries.ZeroFilled({
+      var series = new TimeSeries({
         datapoints: datapoints,
         datapoints: datapoints,
         info: seriesInfo,
         info: seriesInfo,
       });
       });
@@ -297,10 +272,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       return series;
       return series;
     };
     };
 
 
-    $scope.otherPanelInFullscreenMode = function() {
-      return $rootScope.fullscreen && !$scope.fullscreen;
-    };
-
     $scope.render = function(data) {
     $scope.render = function(data) {
       $scope.$emit('render', data);
       $scope.$emit('render', data);
     };
     };
@@ -361,8 +332,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     };
     };
 
 
     $scope.toggleYAxis = function(info) {
     $scope.toggleYAxis = function(info) {
-      info.yaxis = info.yaxis === 2 ? 1 : 2;
-      $scope.panel.aliasYAxis[info.alias] = info.yaxis;
+      var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias });
+      if (!override) {
+        override = { alias: info.alias };
+        $scope.panel.seriesOverrides.push(override);
+      }
+      override.yaxis = info.yaxis === 2 ? 1 : 2;
       $scope.render();
       $scope.render();
     };
     };
 
 
@@ -371,7 +346,16 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       $scope.render();
       $scope.render();
     };
     };
 
 
-    $scope.init();
+    $scope.addSeriesOverride = function() {
+      $scope.panel.seriesOverrides.push({});
+    };
+
+    $scope.removeSeriesOverride = function(override) {
+      $scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override);
+      $scope.render();
+    };
+
+    panelSrv.init($scope);
   });
   });
 
 
 });
 });

+ 80 - 0
src/app/panels/graph/seriesOverridesCtrl.js

@@ -0,0 +1,80 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+], function(angular, app, _) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.graph', []);
+  app.useModule(module);
+
+  module.controller('SeriesOverridesCtrl', function($scope) {
+    $scope.overrideMenu = [];
+    $scope.currentOverrides = [];
+    $scope.override = $scope.override || {};
+
+    $scope.addOverrideOption = function(name, propertyName, values) {
+      var option = {};
+      option.text = name;
+      option.propertyName = propertyName;
+      option.index = $scope.overrideMenu.length;
+      option.values = values;
+
+      option.submenu = _.map(values, function(value, index) {
+        return {
+          text: String(value),
+          click: 'setOverride(' + option.index + ',' + index + ')'
+        };
+      });
+
+      $scope.overrideMenu.push(option);
+    };
+
+    $scope.setOverride = function(optionIndex, valueIndex) {
+      var option = $scope.overrideMenu[optionIndex];
+      var value = option.values[valueIndex];
+      $scope.override[option.propertyName] = value;
+      $scope.updateCurrentOverrides();
+      $scope.render();
+    };
+
+    $scope.removeOverride = function(option) {
+      delete $scope.override[option.propertyName];
+      $scope.updateCurrentOverrides();
+      $scope.render();
+    };
+
+    $scope.getSeriesNames = function() {
+      return _.map($scope.legend, function(info) {
+        return info.alias;
+      });
+    };
+
+    $scope.updateCurrentOverrides = function() {
+      $scope.currentOverrides = [];
+      _.each($scope.overrideMenu, function(option) {
+        var value = $scope.override[option.propertyName];
+        if (_.isUndefined(value)) { return; }
+        $scope.currentOverrides.push({
+          name: option.text,
+          propertyName: option.propertyName,
+          value: String(value)
+        });
+      });
+    };
+
+    $scope.addOverrideOption('Bars', 'bars', [true, false]);
+    $scope.addOverrideOption('Lines', 'lines', [true, false]);
+    $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
+    $scope.addOverrideOption('Points', 'points', [true, false]);
+    $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
+    $scope.addOverrideOption('Stack', 'stack', [true, false]);
+    $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
+    $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
+    $scope.updateCurrentOverrides();
+
+  });
+
+});

+ 46 - 2
src/app/panels/graph/styleEditor.html

@@ -1,5 +1,3 @@
-
-
 <div class="editor-row">
 <div class="editor-row">
   <div class="section">
   <div class="section">
     <h5>Chart Options</h5>
     <h5>Chart Options</h5>
@@ -64,3 +62,49 @@
     </div>
     </div>
   </div>
   </div>
 </div>
 </div>
+
+<div class="editor-row">
+  <div class="section">
+		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
+		<div>
+		<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
+			<div class="grafana-target-inner-wrapper">
+				<div class="grafana-target-inner">
+
+					<ul class="grafana-target-controls-left">
+						<li class="grafana-target-segment">
+							<i class="icon-remove pointer" ng-click="removeSeriesOverride(override)"></i>
+						</li>
+					</ul>
+
+					<ul class="grafana-segment-list">
+						<li class="grafana-target-segment">
+							alias or regex
+						</li>
+						<li>
+							<input type="text"
+										ng-model="override.alias"
+                    bs-typeahead="getSeriesNames"
+										ng-blur="render()"
+										data-min-length=0 data-items=100
+										class="input-medium grafana-target-segment-input" >
+						</li>
+						<li class="grafana-target-segment" ng-repeat="option in currentOverrides">
+							<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
+							{{option.name}}: {{option.value}}
+						</li>
+						<li class="dropdown">
+							<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'">
+								<i class="icon-plus"></i>
+							</a>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+			</div>
+		</div>
+		</div>
+
+		<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
+	</div>
+</div>

+ 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 ng-bind-html="content">
   </p>
   </p>
 </div>
 </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([
 define([
   'angular',
   'angular',
   'app',
   'app',
@@ -23,6 +11,8 @@ function (angular, app, _, require) {
   var module = angular.module('grafana.panels.text', []);
   var module = angular.module('grafana.panels.text', []);
   app.useModule(module);
   app.useModule(module);
 
 
+  var converter;
+
   module.controller('text', function($scope, filterSrv, $sce, panelSrv) {
   module.controller('text', function($scope, filterSrv, $sce, panelSrv) {
 
 
     $scope.panelMeta = {
     $scope.panelMeta = {
@@ -68,15 +58,20 @@ function (angular, app, _, require) {
     };
     };
 
 
     $scope.renderMarkdown = function(content) {
     $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));
         $scope.updateContent(converter.makeHtml(text));
-      });
+      }
+      else {
+        require(['./lib/showdown'], function (Showdown) {
+          converter = new Showdown.converter();
+          $scope.updateContent(converter.makeHtml(text));
+        });
+      }
     };
     };
 
 
     $scope.updateContent = function(html) {
     $scope.updateContent = function(html) {
@@ -88,7 +83,7 @@ function (angular, app, _, require) {
       }
       }
 
 
       if(!$scope.$$phase) {
       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">
     <ul class="nav nav-pills timepicker-dropdown">
       <li class="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-hide="filter.time">Time filter</span>
           <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
           <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
           <i class="icon-caret-down"></i>
           <i class="icon-caret-down"></i>

+ 29 - 6
src/app/panels/timepicker/module.js

@@ -80,9 +80,14 @@ function (angular, app, _, moment, kbn) {
       $scope.temptime = cloneTime($scope.time);
       $scope.temptime = cloneTime($scope.time);
       $scope.tempnow = $scope.panel.now;
       $scope.tempnow = $scope.panel.now;
 
 
+      $scope.temptime.from.date.setHours(0,0,0,0);
+      $scope.temptime.to.date.setHours(0,0,0,0);
+
       // Date picker needs the date to be at the start of the day
       // Date picker needs the date to be at the start of the day
-      $scope.temptime.from.date.setHours(1,0,0,0);
-      $scope.temptime.to.date.setHours(1,0,0,0);
+      if(new Date().getTimezoneOffset() < 0) {
+        $scope.temptime.from.date = moment($scope.temptime.from.date).add('days',1).toDate();
+        $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
+      }
 
 
       $q.when(customTimeModal).then(function(modalEl) {
       $q.when(customTimeModal).then(function(modalEl) {
         modalEl.modal('show');
         modalEl.modal('show');
@@ -172,10 +177,28 @@ function (angular, app, _, moment, kbn) {
     };
     };
 
 
     var getScopeTimeObj = function(from,to) {
     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 = $scope.dashboard.formatDate(model.from.date) + ' <br>to<br>';
+        model.tooltip += $scope.dashboard.formatDate(model.to.date);
+      }
+      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 = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY HH:mm:ss') + ' to ' +
+            $scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY HH:mm:ss');
+        }
+      }
+
+      return model;
     };
     };
 
 
     var getTimeObj = function(date) {
     var getTimeObj = function(date) {

+ 0 - 69
src/app/partials/dashLoader.html

@@ -1,69 +0,0 @@
-<style>
-  .noarrow>a:after {
-    display: none !important;
-  }
-</style>
-
-<li ng-show="fullscreen">
-  <a ng-click="exitFullscreen()">
-    Back to dashboard
-  </a>
-</li>
-
-<li class="grafana-menu-zoom-out">
-  <a class='small' ng-click='zoom(2)'>
-    Zoom Out
-  </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 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">
-
-    <li>
-      <form class="input-prepend nomargin save-dashboard-dropdown-save-form">
-        <input class='input-medium' ng-model="dashboard.title" type="text" />
-        <button class="btn" ng-click="saveDashboard()"><i class="icon-save"></i></button>
-      </form>
-    </li>
-
-    <li>
-      <a class="link" ng-click="set_default()">Save as Home</a>
-    </li>
-    <li>
-      <a class="link" ng-click="purge_default()">Reset Home</a>
-    </li>
-    <li ng-show="!isFavorite">
-      <a class="link" ng-click="markAsFavorite()">Mark as favorite</a>
-    </li>
-    <li ng-show="isFavorite">
-      <a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
-    </li>
-    <li>
-      <a class="link" ng-click="exportDashboard()">Export dashboard</a>
-    </li>
-		<li ng-show="db.saveTemp">
-			<a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">
-				Share temp copy
-			</a>
-	  </li>
-  </ul>
-</li>
-
-<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
-</li>
-
-<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
-
-<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
-
-<li class="grafana-menu-stop-playlist hide">
-  <a class='small' ng-click='stopPlaylist(2)'>
-    Stop playlist
-  </a>
-</li>

+ 16 - 18
src/app/partials/dashboard.html

@@ -1,16 +1,9 @@
-<div ng-controller="DashCtrl" body-class>
+<div ng-controller="DashboardCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
 
 
-  <div class="navbar navbar-static-top">
-    <div class="navbar-inner">
-      <div class="container-fluid">
-        <span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
-        <ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
-        </ul>
-      </div>
-    </div>
-  </div>
+	<div ng-include="'app/partials/dashboard_topnav.html'">
+	</div>
 
 
-  <div class="submenu-controls">
+	<div class="submenu-controls">
     <div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
     <div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
       <div class="submenu-panel-title">
       <div class="submenu-panel-title">
         <span class="small"><strong>{{pulldown.type}}:</strong></span>
         <span class="small"><strong>{{pulldown.type}}:</strong></span>
@@ -27,7 +20,7 @@
     <div>
     <div>
       <div class="grafana-container container">
       <div class="grafana-container container">
         <!-- Rows -->
         <!-- 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">
             <div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
             <div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
               <div class="row-close" ng-show="row.collapse" data-placement="bottom" >
               <div class="row-close" ng-show="row.collapse" data-placement="bottom" >
@@ -98,14 +91,19 @@
 							</div>
 							</div>
 
 
               <!-- Panels -->
               <!-- 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>
 
 
-              <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>
 
 
               <div class="clearfix"></div>
               <div class="clearfix"></div>

+ 77 - 0
src/app/partials/dashboard_topnav.html

@@ -0,0 +1,77 @@
+<div class="navbar navbar-static-top">
+	<div class="navbar-inner">
+		<div class="container-fluid">
+			<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
+			<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
+
+				<li ng-show="dashboardViewState.fullscreen">
+					<a ng-click="exitFullscreen()">
+						Back to dashboard
+					</a>
+				</li>
+
+				<li class="grafana-menu-zoom-out">
+					<a class='small' ng-click='zoom(2)'>
+						Zoom Out
+					</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 class="dropdown grafana-menu-save">
+					<a 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" ng-if="saveDropdownOpened">
+						<li>
+							<form class="input-prepend nomargin save-dashboard-dropdown-save-form">
+								<input class='input-medium' ng-model="dashboard.title" type="text" />
+								<button class="btn" ng-click="saveDashboard()"><i class="icon-save"></i></button>
+							</form>
+						</li>
+
+						<li>
+							<a class="link" ng-click="set_default()">Save as Home</a>
+						</li>
+						<li>
+							<a class="link" ng-click="purge_default()">Reset Home</a>
+						</li>
+						<li ng-show="!isFavorite">
+							<a class="link" ng-click="markAsFavorite()">Mark as favorite</a>
+						</li>
+						<li ng-show="isFavorite">
+							<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
+						</li>
+						<li>
+							<a class="link" ng-click="exportDashboard()">Export dashboard</a>
+						</li>
+						<li ng-show="db.saveTemp">
+							<a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">
+								Share temp copy
+							</a>
+						</li>
+					</ul>
+				</li>
+
+				<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
+				</li>
+
+				<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
+
+				<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
+
+				<li class="grafana-menu-stop-playlist hide">
+					<a class='small' ng-click='stopPlaylist(2)'>
+						Stop playlist
+					</a>
+				</li>
+
+			</ul>
+		</div>
+	</div>
+</div>
+

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

@@ -68,7 +68,7 @@
     </div>
     </div>
   </div>
   </div>
 
 
-  <div ng-if="editor.index == 2" ng-controller="dashLoader">
+  <div ng-if="editor.index == 2">
     <div class="editor-row">
     <div class="editor-row">
 			<div class="section">
 			<div class="section">
 				<h5>Feature toggles</h5>
 				<h5>Feature toggles</h5>

+ 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 class="pull-right editor-title">Row settings</div>
 
 
   <div ng-model="editor.index" bs-tabs>
   <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>
   </div>
   </div>
 
 
@@ -27,11 +27,10 @@
         <thead>
         <thead>
           <th>Title</th>
           <th>Title</th>
           <th>Type</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>Delete</th>
           <th>Move</th>
           <th>Move</th>
           <th></th>
           <th></th>
-          <th>Hide</th>
         </thead>
         </thead>
         <tr ng-repeat="panel in row.panels">
         <tr ng-repeat="panel in row.panels">
           <td>{{panel.title}}</td>
           <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="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="$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><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>
         </tr>
       </table>
       </table>
     </div>
     </div>
   </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>
 <div class="modal-footer">
 <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>
   <button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>

+ 39 - 43
src/app/partials/search.html

@@ -16,7 +16,7 @@
     <i class='icon-folder-open'></i>
     <i class='icon-folder-open'></i>
   </a>
   </a>
 
 
-  <ul class="dropdown-menu" id="grafana-search">
+  <ul class="dropdown-menu" id="grafana-search" ng-if="searchOpened">
     <li ng-if="!showImport">
     <li ng-if="!showImport">
       <div class="grafana-search-panel">
       <div class="grafana-search-panel">
         <div class="search-field-wrapper">
         <div class="search-field-wrapper">
@@ -47,50 +47,46 @@
 
 
         <h6 ng-hide="results.dashboards.length || results.metrics.length">No dashboards or metrics matching your query found</h6>
         <h6 ng-hide="results.dashboards.length || results.metrics.length">No dashboards or metrics matching your query found</h6>
 
 
-        <table class="table table-condensed table-striped" ng-if="tagsOnly">
-          <tr ng-repeat="tag in results.tags" ng-class="{'selected-tag': $index === selectedIndex }">
-            <td>
-              <a ng-click="filterByTag(tag.term, $event)" class="label label-tag">
-                {{tag.term}} &nbsp;({{tag.count}})
-              </a>
-            </td>
-            <td style="width:100%;padding-left: 10px;font-weight: bold;">
-            </td>
-          </tr>
-        </table>
+        <div class="search-results-container" ng-if="tagsOnly">
+					<div ng-repeat="tag in results.tags"
+						   class="search-result-item pointer"
+							 ng-class="{'selected': $index === selectedIndex }"
+							 ng-click="filterByTag(tag.term, $event)">
+						<a class="search-result-link" >
+							<i class="icon icon-tag"></i>
+							<span class="label label-tag">{{tag.term}} &nbsp;({{tag.count}})</span>
+						</a>
+          </div>
+        </div>
+
+				<div class="search-results-container" ng-if="!tagsOnly">
+					<div class="search-result-item pointer"
+							bindonce ng-repeat="row in results.dashboards"
+							ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
+
+							<div class="search-result-actions">
+								<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
+									<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
+								</a>
+								<a ng-click="deleteDashboard(row.id, $event)">
+									<i class="icon-remove"></i> delete
+								</a>
+							</div>
+
+							<div class="search-result-tags">
+								<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" style="margin-right: 5px;" class="label label-tag">
+									{{tag}}
+								</a>
+							</div>
+
+							<a class="search-result-link">
+								<i class="icon icon-th-large"></i>
+								<span bo-text="row.id"></span>
+							</a>
 
 
-        <table class="table table-condensed table-striped" ng-if="!tagsOnly">
-					<tbody style="max-height: 570px; overflow: auto; display: block">
-          <tr bindonce ng-repeat="row in results.metrics"
-              class="grafana-search-metric-result"
-              ng-class="{'selected': $index === selectedIndex }">
-            <td><span class="label label-info">metric</span></td>
-            <td class="grafana-search-metric-name">
-              {{row.id}}
-            </td>
-            <td style="width:100%;">
-              <div class="grafana-search-metric-actions">
-                <a ng-click="addMetricToCurrentDashboard(row.id)"><i class="icon-plus-sign"></i> Add to dashboard</a>
-              </div>
-            </td>
-          </tr>
+					</div>
+				</div>
 
 
-          <tr bindonce
-              ng-repeat="row in results.dashboards"
-              ng-class="{'selected': $index === selectedIndex }">
-            <td><a ng-click="deleteDashboard(row.id)"><i class="icon-remove"></i></a></td>
-            <td style="width:100%">
-              <a href="#/dashboard/db/{{row.id}}" bo-text="row.id"></a>
-            </td>
-            <td style="white-space: nowrap; text-align: right;">
-              <a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" style="margin-right: 5px;" class="label label-tag">
-                {{tag}}
-              </a>
-            </td>
-            <td><a><i class="icon-share" ng-click="shareDashboard(row.id, row.id)" config-modal="app/partials/dashLoaderShare.html"></i></a></td>
-          </tr>
-					</tbody>
-        </table>
       </div>
       </div>
     </li>
     </li>
 
 

+ 4 - 8
src/app/routes/dashboard-default.js

@@ -1,8 +1,9 @@
 define([
 define([
   'angular',
   'angular',
-  'config'
+  'config',
+  'store'
 ],
 ],
-function (angular, config) {
+function (angular, config, store) {
   "use strict";
   "use strict";
 
 
   var module = angular.module('grafana.routes');
   var module = angular.module('grafana.routes');
@@ -11,12 +12,7 @@ function (angular, config) {
     $routeProvider
     $routeProvider
       .when('/', {
       .when('/', {
         redirectTo: function() {
         redirectTo: function() {
-          if (window.localStorage && window.localStorage.grafanaDashboardDefault) {
-            return window.localStorage.grafanaDashboardDefault;
-          }
-          else {
-            return config.default_route;
-          }
+          return store.get('grafanaDashboardDefault') || config.default_route;
         }
         }
       });
       });
   });
   });

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

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

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

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

+ 2 - 16
src/app/services/dashboard/dashboardKeyBindings.js

@@ -12,28 +12,14 @@ function(angular, $) {
 
 
     this.shortcuts = function(scope) {
     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() {
       scope.$on('$destroy', function() {
         keyboardManager.unbind('ctrl+f');
         keyboardManager.unbind('ctrl+f');
         keyboardManager.unbind('ctrl+h');
         keyboardManager.unbind('ctrl+h');
         keyboardManager.unbind('ctrl+s');
         keyboardManager.unbind('ctrl+s');
         keyboardManager.unbind('ctrl+r');
         keyboardManager.unbind('ctrl+r');
         keyboardManager.unbind('ctrl+z');
         keyboardManager.unbind('ctrl+z');
-        keyboardManager.unbind('esc');
       });
       });
+      keyboardManager.unbind('esc');
 
 
       keyboardManager.bind('ctrl+f', function(evt) {
       keyboardManager.bind('ctrl+f', function(evt) {
         scope.emitAppEvent('open-search', evt);
         scope.emitAppEvent('open-search', evt);
@@ -67,7 +53,7 @@ function(angular, $) {
           modalData.$scope.dismiss();
           modalData.$scope.dismiss();
         }
         }
 
 
-        scope.emitAppEvent('panel-fullscreen-exit');
+        scope.exitFullscreen();
       }, { inputDisabled: true });
       }, { inputDisabled: true });
     };
     };
   });
   });

+ 144 - 38
src/app/services/dashboard/dashboardSrv.js

@@ -3,14 +3,15 @@ define([
   'jquery',
   'jquery',
   'kbn',
   'kbn',
   'lodash',
   'lodash',
+  'moment',
   '../timer',
   '../timer',
 ],
 ],
-function (angular, $, kbn, _) {
+function (angular, $, kbn, _, moment) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('dashboardSrv', function(timer, $rootScope, $timeout) {
+  module.factory('dashboardSrv', function(timer, $rootScope, $timeout) {
 
 
     function DashboardModel (data) {
     function DashboardModel (data) {
 
 
@@ -29,6 +30,8 @@ function (angular, $, kbn, _) {
       this.time = data.time || { from: 'now-6h', to: 'now' };
       this.time = data.time || { from: 'now-6h', to: 'now' };
       this.templating = data.templating || { list: [] };
       this.templating = data.templating || { list: [] };
       this.refresh = data.refresh;
       this.refresh = data.refresh;
+      this.version = data.version || 0;
+      this.$state = data.$state;
 
 
       if (this.nav.length === 0) {
       if (this.nav.length === 0) {
         this.nav.push({ type: 'timepicker' });
         this.nav.push({ type: 'timepicker' });
@@ -47,6 +50,73 @@ function (angular, $, kbn, _) {
 
 
     var p = DashboardModel.prototype;
     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.formatDate = function(date, format) {
+      format = format || 'YYYY-MM-DD HH:mm:ss';
+
+      return this.timezone === 'browser' ?
+              moment(date).format(format) :
+              moment.utc(date).format(format);
+    };
+
     p.emit_refresh = function() {
     p.emit_refresh = function() {
       $rootScope.$broadcast('refresh');
       $rootScope.$broadcast('refresh');
     };
     };
@@ -74,61 +144,97 @@ function (angular, $, kbn, _) {
     };
     };
 
 
     p.updateSchema = function(old) {
     p.updateSchema = function(old) {
-      var i, j, row, panel;
-      var isChanged = false;
+      var oldVersion = this.version;
+      var panelUpgrades = [];
+      this.version = 4;
 
 
-      if (this.version === 2) {
+      if (oldVersion === 4) {
         return;
         return;
       }
       }
 
 
-      if (old.services) {
-        if (old.services.filter) {
-          this.time = old.services.filter.time;
-          this.templating.list = old.services.filter.list;
+      // version 2 schema changes
+      if (oldVersion < 2) {
+
+        if (old.services) {
+          if (old.services.filter) {
+            this.time = old.services.filter.time;
+            this.templating.list = old.services.filter.list;
+          }
+          delete this.services;
         }
         }
-        delete this.services;
-      }
 
 
-      for (i = 0; i < this.rows.length; i++) {
-        row = this.rows[i];
-        for (j = 0; j < row.panels.length; j++) {
-          panel = row.panels[j];
+        panelUpgrades.push(function(panel) {
+          // rename panel type
           if (panel.type === 'graphite') {
           if (panel.type === 'graphite') {
             panel.type = 'graph';
             panel.type = 'graph';
-            isChanged = true;
           }
           }
 
 
-          if (panel.type === 'graph') {
-            if (_.isBoolean(panel.legend)) {
-              panel.legend = { show: panel.legend };
-            }
+          if (panel.type !== 'graph') {
+            return;
+          }
 
 
-            if (panel.grid) {
-              if (panel.grid.min) {
-                panel.grid.leftMin = panel.grid.min;
-                delete panel.grid.min;
-              }
+          if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
 
 
-              if (panel.grid.max) {
-                panel.grid.leftMax = panel.grid.max;
-                delete panel.grid.max;
-              }
+          if (panel.grid) {
+            if (panel.grid.min) {
+              panel.grid.leftMin = panel.grid.min;
+              delete panel.grid.min;
             }
             }
 
 
-            if (panel.y_format) {
-              panel.y_formats[0] = panel.y_format;
-              delete panel.y_format;
+            if (panel.grid.max) {
+              panel.grid.leftMax = panel.grid.max;
+              delete panel.grid.max;
             }
             }
+          }
 
 
-            if (panel.y2_format) {
-              panel.y_formats[1] = panel.y2_format;
-              delete panel.y2_format;
-            }
+          if (panel.y_format) {
+            panel.y_formats[0] = panel.y_format;
+            delete panel.y_format;
           }
           }
-        }
+
+          if (panel.y2_format) {
+            panel.y_formats[1] = panel.y2_format;
+            delete panel.y2_format;
+          }
+        });
+      }
+
+      // schema version 3 changes
+      if (oldVersion < 3) {
+        // ensure panel ids
+        var maxId = this.getNextPanelId();
+        panelUpgrades.push(function(panel) {
+          if (!panel.id) {
+            panel.id = maxId;
+            maxId += 1;
+          }
+        });
+      }
+
+      // schema version 4 changes
+      if (oldVersion < 4) {
+        // move aliasYAxis changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'graph') { return; }
+          _.each(panel.aliasYAxis, function(value, key) {
+            panel.seriesOverrides = [{ alias: key, yaxis: value }];
+          });
+          delete panel.aliasYAxis;
+        });
       }
       }
 
 
-      this.version = 2;
+      if (panelUpgrades.length === 0) {
+        return;
+      }
+
+      for (var i = 0; i < this.rows.length; i++) {
+        var row = this.rows[i];
+        for (var j = 0; j < row.panels.length; j++) {
+          for (var k = 0; k < panelUpgrades.length; k++) {
+            panelUpgrades[k](row.panels[j]);
+          }
+        }
+      }
     };
     };
 
 
     return {
     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':
       case 'grafana':
         Datasource = $injector.get('GrafanaDatasource');
         Datasource = $injector.get('GrafanaDatasource');
         break;
         break;
+      default:
+        Datasource = $injector.get(ds.type);
       }
       }
       return new Datasource(ds);
       return new Datasource(ds);
     };
     };

+ 2 - 2
src/app/services/graphite/graphiteDatasource.js

@@ -150,7 +150,7 @@ function (angular, _, $, config, kbn, moment) {
 
 
       if (rounding === 'round-up') {
       if (rounding === 'round-up') {
         if (date.get('s')) {
         if (date.get('s')) {
-          date.add('m', 1);
+          date.add(1, 'm');
         }
         }
       }
       }
       else if (rounding === 'round-down') {
       else if (rounding === 'round-down') {
@@ -159,7 +159,7 @@ function (angular, _, $, config, kbn, moment) {
         // to guarantee that we get all the data that
         // to guarantee that we get all the data that
         // exists for the specified range
         // exists for the specified range
         if (date.get('s')) {
         if (date.get('s')) {
-          date.subtract('m', 1);
+          date.subtract(1, 'm');
         }
         }
       }
       }
 
 

+ 3 - 0
src/app/services/graphite/lexer.js

@@ -124,6 +124,9 @@ define([
       i === 45 ||           // -
       i === 45 ||           // -
       i === 42 ||           // *
       i === 42 ||           // *
       i === 58 ||           // :
       i === 58 ||           // :
+      i === 91 ||           // templateStart [
+      i === 93 ||           // templateEnd ]
+      i === 63 ||           // ?
       i === 37 ||           // %
       i === 37 ||           // %
       i >= 97 && i <= 122;  // a-z
       i >= 97 && i <= 122;  // a-z
   }
   }

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

@@ -1,9 +1,8 @@
 define([
 define([
   'angular',
   'angular',
   'lodash',
   'lodash',
-  'jquery',
 ],
 ],
-function (angular, _, $) {
+function (angular, _) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
@@ -22,12 +21,12 @@ function (angular, _, $) {
         },
         },
         {
         {
           text: 'Edit',
           text: 'Edit',
-          click: "toggleFullscreenEdit()",
+          click: "toggleFullscreen(true)",
           condition: $scope.panelMeta.fullscreenEdit
           condition: $scope.panelMeta.fullscreenEdit
         },
         },
         {
         {
           text: "Fullscreen",
           text: "Fullscreen",
-          click: 'toggleFullscreen()',
+          click: 'toggleFullscreen(false)',
           condition: $scope.panelMeta.fullscreenView
           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.addDataQuery = function() {
         $scope.panel.targets.push({target: ''});
         $scope.panel.targets.push({target: ''});
       };
       };
@@ -135,22 +94,12 @@ function (angular, _, $) {
         $scope.get_data();
         $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
       // Post init phase
@@ -162,6 +111,31 @@ function (angular, _, $) {
 
 
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.setDatasource($scope.panel.datasource);
       $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();
+        }
+      }
     };
     };
   });
   });
 
 

+ 6 - 4
src/app/services/playlistSrv.js

@@ -1,9 +1,10 @@
 define([
 define([
   'angular',
   'angular',
   'lodash',
   'lodash',
-  'kbn'
+  'kbn',
+  'store'
 ],
 ],
-function (angular, _, kbn) {
+function (angular, _, kbn, store) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
@@ -13,14 +14,14 @@ function (angular, _, kbn) {
     var favorites = { dashboards: [] };
     var favorites = { dashboards: [] };
 
 
     this.init = function() {
     this.init = function() {
-      var existingJson = window.localStorage["grafana-favorites"];
+      var existingJson = store.get("grafana-favorites");
       if (existingJson) {
       if (existingJson) {
         favorites = angular.fromJson(existingJson);
         favorites = angular.fromJson(existingJson);
       }
       }
     };
     };
 
 
     this._save = function() {
     this._save = function() {
-      window.localStorage["grafana-favorites"] = angular.toJson(favorites);
+      store.set('grafana-favorites', angular.toJson(favorites));
     };
     };
 
 
     this._find = function(title) {
     this._find = function(title) {
@@ -68,6 +69,7 @@ function (angular, _, kbn) {
       timerInstance = setInterval(function() {
       timerInstance = setInterval(function() {
         $rootScope.$apply(function() {
         $rootScope.$apply(function() {
           angular.element(window).unbind('resize');
           angular.element(window).unbind('resize');
+          $location.search({});
           $location.path(dashboards[index % dashboards.length].url);
           $location.path(dashboards[index % dashboards.length].url);
           index++;
           index++;
         });
         });

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

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

+ 5 - 1
src/config.sample.js

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

ファイルの差分が大きいため隠しています
+ 0 - 8
src/css/bootstrap.dark.min.css


ファイルの差分が大きいため隠しています
+ 0 - 8
src/css/bootstrap.light.min.css


+ 44 - 12
src/css/less/grafana.less

@@ -35,9 +35,8 @@
 // Search
 // Search
 
 
 .grafana-search-panel {
 .grafana-search-panel {
-  padding: 6px 10px;
-
   .search-field-wrapper {
   .search-field-wrapper {
+    padding: 6px 10px;
     input {
     input {
       width: 100%;
       width: 100%;
     }
     }
@@ -50,25 +49,58 @@
       padding-right: 25px;
       padding-right: 25px;
     }
     }
   }
   }
+}
+
+.search-results-container {
+  max-height: 600px;
+  overflow: auto;
+  display: block;
+  .search-result-item a {
+  }
+
+  .search-result-item:hover, .search-result-item.selected {
+    .search-result-link, .icon {
+      color: @grafanaListHighlight;
+    }
+    .search-result-link .label {
+      background-color: @blue;
+    }
+  }
+
 
 
-  .selected td, tr.selected:nth-child(odd)>td {
-    background: @blue;
-    color: white;
-    text-shadow: -1px -1px 1px rgba(0,0,0,0.3);
-    a {
-      color: white;
+  .search-result-link {
+    color: @grafanaListMainLinkColor;
+    .icon {
+      padding-right: 10px;
+      color: @grafanaListHighlightContrast;
     }
     }
   }
   }
 
 
-  .selected-tag .label-tag {
-    background-color: @blue;
+  .search-result-item:nth-child(odd) {
+    background-color: @grafanaListAccent;
+  }
+
+  .search-result-item {
+    padding: 6px 10px;
+    white-space: nowrap;
+    border-top: 1px solid @grafanaListBorderTop;
+    border-bottom: 1px solid @grafanaListBorderBottom;
+  }
+
+  .search-result-tags {
+    float: right;
+  }
+
+  .search-result-actions {
+    float: right;
+    padding-left: 10px;
   }
   }
 }
 }
 
 
 .search-tagview-switch {
 .search-tagview-switch {
   position: absolute;
   position: absolute;
   top: 15px;
   top: 15px;
-  right: 263px;
+  right: 266px;
   color: darken(@linkColor, 30%);
   color: darken(@linkColor, 30%);
   &.active {
   &.active {
     color: @linkColor;
     color: @linkColor;
@@ -155,7 +187,7 @@
 }
 }
 
 
 .panel-full-edit-tabs {
 .panel-full-edit-tabs {
-  margin-top: 10px;
+  margin-top: 30px;
   min-height: 250px;
   min-height: 250px;
   margin-left: -10px;
   margin-left: -10px;
   margin-right: -10px;
   margin-right: -10px;

+ 17 - 1
src/css/less/graph.less

@@ -4,7 +4,7 @@
 
 
 .graph-legend {
 .graph-legend {
   margin: 0 20px;
   margin: 0 20px;
-  text-align: left;
+  text-align: center;
   position: relative;
   position: relative;
   top: 2px;
   top: 2px;
 
 
@@ -25,6 +25,7 @@
 .graph-legend-value {
 .graph-legend-value {
   float: left;
   float: left;
   white-space: nowrap;
   white-space: nowrap;
+  text-align: left;
   &.current:before {
   &.current:before {
     content: "Current: "
     content: "Current: "
   }
   }
@@ -44,6 +45,7 @@
 
 
 .graph-legend-series {
 .graph-legend-series {
   padding-left: 10px;
   padding-left: 10px;
+  padding-top: 2px;
 }
 }
 
 
 .graph-legend-value {
 .graph-legend-value {
@@ -152,3 +154,17 @@
 .annotation-tags {
 .annotation-tags {
   color: @purple;
   color: @purple;
 }
 }
+
+.graph-series-override {
+  input {
+    float: left;
+    margin-right: 10px;
+  }
+  .graph-series-override-option {
+    float: left;
+    padding: 2px 6px;
+  }
+  .graph-series-override-selector {
+    float: left;
+  }
+}

+ 9 - 1
src/css/less/variables.dark.less

@@ -57,7 +57,7 @@
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
-@linkColor:             darken(@white,5%);
+@linkColor:             darken(@white,11%);
 @linkColorHover:        @white;
 @linkColorHover:        @white;
 
 
 
 
@@ -93,6 +93,14 @@
 @borderRadiusLarge:     4px;
 @borderRadiusLarge:     4px;
 @borderRadiusSmall:     2px;
 @borderRadiusSmall:     2px;
 
 
+// Lists
+@grafanaListBackground:  transparent;
+@grafanaListAccent:         #232323;
+@grafanaListBorderTop:      #3E3E3E;
+@grafanaListBorderBottom:   #1c1919;
+@grafanaListHighlight:      @blue;
+@grafanaListHighlightContrast: #4F4F4F;
+@grafanaListMainLinkColor: @linkColor;
 
 
 // Tables
 // Tables
 // -------------------------
 // -------------------------

+ 10 - 1
src/css/less/variables.light.less

@@ -20,7 +20,7 @@
 
 
 // Accent colors
 // Accent colors
 // -------------------------
 // -------------------------
-@blue:                  #01A6E6;
+@blue:                  #007FFF;
 @blueDark:              #75CAEB;
 @blueDark:              #75CAEB;
 @green:                 #28B62C;
 @green:                 #28B62C;
 @red:                   #FF4136;
 @red:                   #FF4136;
@@ -97,6 +97,15 @@
 @borderRadiusLarge:     4px;
 @borderRadiusLarge:     4px;
 @borderRadiusSmall:     2px;
 @borderRadiusSmall:     2px;
 
 
+// Lists
+@grafanaListBackground:  transparent;
+@grafanaListAccent:         #f9f9f9;
+@grafanaListBorderTop:      #eee;
+@grafanaListBorderBottom:   #efefef;
+@grafanaListHighlight:      @blue;
+@grafanaListHighlightContrast: #ddd;
+@grafanaListMainLinkColor: @textColor;
+
 
 
 // Tables
 // Tables
 // -------------------------
 // -------------------------

+ 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>
       <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>
 
 
-    <div ng-view ng-class="{'dashboard-fullscreen': fullscreen}"></div>
+    <div ng-view></div>
 
 
   </body>
   </body>
 </html>
 </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;
+
+  });
+
+});

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

@@ -7,7 +7,6 @@ define([
     var model;
     var model;
 
 
     beforeEach(module('grafana.services'));
     beforeEach(module('grafana.services'));
-
     beforeEach(inject(function(dashboardSrv) {
     beforeEach(inject(function(dashboardSrv) {
       model = dashboardSrv.create({});
       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() {
   describe('when creating dashboard with old schema', function() {
     var model;
     var model;
     var graph;
     var graph;
 
 
     beforeEach(module('grafana.services'));
     beforeEach(module('grafana.services'));
-
     beforeEach(inject(function(dashboardSrv) {
     beforeEach(inject(function(dashboardSrv) {
       model = dashboardSrv.create({
       model = dashboardSrv.create({
         services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [1] }},
         services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [1] }},
@@ -39,6 +97,7 @@ define([
               {
               {
                 type: 'graphite',
                 type: 'graphite',
                 legend: true,
                 legend: true,
+                aliasYAxis: { test: 2 },
                 grid: { min: 1, max: 10 }
                 grid: { min: 1, max: 10 }
               }
               }
             ]
             ]
@@ -54,6 +113,10 @@ define([
       expect(model.title).to.be('No Title');
       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() {
     it('should move time and filtering list', function() {
       expect(model.time.from).to.be('now-1d');
       expect(model.time.from).to.be('now-1d');
       expect(model.templating.list[0]).to.be(1);
       expect(model.templating.list[0]).to.be(1);
@@ -72,11 +135,15 @@ define([
       expect(graph.grid.leftMax).to.be(10);
       expect(graph.grid.leftMax).to.be(10);
     });
     });
 
 
+    it('move aliasYAxis to series override', function() {
+      expect(graph.seriesOverrides[0].alias).to.be("test");
+      expect(graph.seriesOverrides[0].yaxis).to.be(2);
+    });
+
     it('dashboard schema version should be set to latest', function() {
     it('dashboard schema version should be set to latest', function() {
-      expect(model.version).to.be(2);
+      expect(model.version).to.be(4);
     });
     });
 
 
   });
   });
 
 
-
 });
 });

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

+ 147 - 0
src/test/specs/grafanaGraph-specs.js

@@ -0,0 +1,147 @@
+define([
+  './helpers',
+  'angular',
+  'jquery',
+  'components/timeSeries',
+  'directives/grafanaGraph'
+], function(helpers, angular, $, TimeSeries) {
+  'use strict';
+
+  describe('grafanaGraph', function() {
+
+    beforeEach(module('grafana.directives'));
+
+    function graphScenario(desc, func)  {
+      describe(desc, function() {
+        var ctx = {};
+        ctx.setup = function (setupFunc) {
+          beforeEach(inject(function($rootScope, $compile) {
+            var scope = $rootScope.$new();
+            var element = angular.element("<div style='width:500px' grafana-graph><div>");
+
+            scope.height = '200px';
+            scope.panel = {
+              legend: {},
+              grid: {},
+              y_formats: [],
+              seriesOverrides: []
+            };
+            scope.dashboard = { timezone: 'browser' };
+            scope.range = {
+              from: new Date('2014-08-09 10:00:00'),
+              to: new Date('2014-09-09 13:00:00')
+            };
+            ctx.data = [];
+            ctx.data.push(new TimeSeries({
+              datapoints: [[1,1],[2,2]],
+              info: { alias: 'series1', enable: true }
+            }));
+            ctx.data.push(new TimeSeries({
+              datapoints: [[1,1],[2,2]],
+              info: { alias: 'series2', enable: true }
+            }));
+
+            setupFunc(scope, ctx.data);
+
+            $compile(element)(scope);
+            scope.$digest();
+            $.plot = ctx.plotSpy = sinon.spy();
+
+            scope.$emit('render', ctx.data);
+            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
+            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
+          }));
+        };
+
+        func(ctx);
+      });
+    }
+
+    graphScenario('simple lines options', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.lines = true;
+        scope.panel.fill = 5;
+        scope.panel.linewidth = 3;
+        scope.panel.steppedLine = true;
+      });
+
+      it('should configure plot with correct options', function() {
+        expect(ctx.plotOptions.series.lines.show).to.be(true);
+        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+        expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
+        expect(ctx.plotOptions.series.lines.steps).to.be(true);
+      });
+    });
+
+    graphScenario('grid thresholds 100, 200', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.grid = {
+          threshold1: 100,
+          threshold1Color: "#111",
+          threshold2: 200,
+          threshold2Color: "#222",
+        };
+      });
+
+      it('should add grid markings', function() {
+        var markings = ctx.plotOptions.grid.markings;
+        expect(markings[0].yaxis.from).to.be(100);
+        expect(markings[0].yaxis.to).to.be(200);
+        expect(markings[0].color).to.be('#111');
+        expect(markings[1].yaxis.from).to.be(200);
+        expect(markings[1].yaxis.to).to.be(Infinity);
+      });
+    });
+
+    graphScenario('inverted grid thresholds 200, 100', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.grid = {
+          threshold1: 200,
+          threshold1Color: "#111",
+          threshold2: 100,
+          threshold2Color: "#222",
+        };
+      });
+
+      it('should add grid markings', function() {
+        var markings = ctx.plotOptions.grid.markings;
+        expect(markings[0].yaxis.from).to.be(200);
+        expect(markings[0].yaxis.to).to.be(100);
+        expect(markings[0].color).to.be('#111');
+        expect(markings[1].yaxis.from).to.be(100);
+        expect(markings[1].yaxis.to).to.be(-Infinity);
+      });
+    });
+
+    graphScenario('series option overrides, fill & points', function(ctx) {
+      ctx.setup(function(scope, data) {
+        scope.panel.lines = true;
+        scope.panel.fill = 5;
+        scope.panel.seriesOverrides = [
+          { alias: 'test', fill: 0, points: true }
+        ];
+
+        data[1].info.alias = 'test';
+      });
+
+      it('should match second series and fill zero, and enable points', function() {
+        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+        expect(ctx.plotData[1].lines.fill).to.be(0.001);
+        expect(ctx.plotData[1].points.show).to.be(true);
+      });
+    });
+
+    graphScenario('should order series order according to zindex', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
+      });
+
+      it('should move zindex 2 last', function() {
+        expect(ctx.plotData[0].info.alias).to.be('series2');
+        expect(ctx.plotData[1].info.alias).to.be('series1');
+      });
+    });
+
+  });
+});
+

+ 9 - 2
src/test/specs/helpers.js

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

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

@@ -22,6 +22,24 @@ 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');
+    });
+    it('should format 0 correctly', function() {
+      var str = kbn.getFormatFunction('')(0.0, { tickDecimals: 12 });
+      expect(str).to.be('0');
+    });
+  });
+
+  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 () {
   describe('nanosecond formatting', function () {
 
 
     it('should translate 25 to 25 ns', function () {
     it('should translate 25 to 25 ns', function () {

+ 11 - 4
src/test/specs/lexer-specs.js

@@ -71,10 +71,17 @@ define([
     it('should tokenize metric with template parameter', function() {
     it('should tokenize metric with template parameter', function() {
       var lexer = new Lexer("metric.[[server]].test");
       var lexer = new Lexer("metric.[[server]].test");
       var tokens = lexer.tokenize();
       var tokens = lexer.tokenize();
-      expect(tokens[2].type).to.be('templateStart');
-      expect(tokens[3].type).to.be('identifier');
-      expect(tokens[3].value).to.be('server');
-      expect(tokens[4].type).to.be('templateEnd');
+      expect(tokens[2].type).to.be('identifier');
+      expect(tokens[2].value).to.be('[[server]]');
+      expect(tokens[4].type).to.be('identifier');
+    });
+
+    it('should tokenize metric with question mark', function() {
+      var lexer = new Lexer("metric.server_??.test");
+      var tokens = lexer.tokenize();
+      expect(tokens[2].type).to.be('identifier');
+      expect(tokens[2].value).to.be('server_??');
+      expect(tokens[4].type).to.be('identifier');
     });
     });
 
 
     it('should handle error with unterminated string', function() {
     it('should handle error with unterminated string', function() {

+ 2 - 2
src/test/specs/parser-specs.js

@@ -106,8 +106,8 @@ define([
 
 
       expect(rootNode.message).to.be(undefined);
       expect(rootNode.message).to.be(undefined);
       expect(rootNode.params[0].type).to.be('metric');
       expect(rootNode.params[0].type).to.be('metric');
-      expect(rootNode.params[0].segments[1].type).to.be('template');
-      expect(rootNode.params[0].segments[1].value).to.be('server');
+      expect(rootNode.params[0].segments[1].type).to.be('segment');
+      expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
     });
     });
 
 
     it('invalid metric expression', function() {
     it('invalid metric expression', function() {

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

@@ -12,50 +12,6 @@ define([
     beforeEach(ctx.providePhase());
     beforeEach(ctx.providePhase());
     beforeEach(ctx.createControllerPhase('RowCtrl'));
     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');
-      });
-    });
-
   });
   });
 
 
 });
 });

+ 51 - 0
src/test/specs/seriesOverridesCtrl-specs.js

@@ -0,0 +1,51 @@
+define([
+  './helpers',
+  'panels/graph/seriesOverridesCtrl'
+], function(helpers) {
+  'use strict';
+
+  describe('SeriesOverridesCtrl', function() {
+    var ctx = new helpers.ControllerTestContext();
+
+    beforeEach(module('grafana.services'));
+    beforeEach(module('grafana.panels.graph'));
+
+    beforeEach(ctx.providePhase());
+    beforeEach(ctx.createControllerPhase('SeriesOverridesCtrl'));
+    beforeEach(function() {
+      ctx.scope.render = function() {};
+    });
+
+    describe('Controller should init overrideMenu', function() {
+      it('click should include option and value index', function() {
+        expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('setOverride(1,1)');
+      });
+    });
+
+    describe('When setting an override', function() {
+      beforeEach(function() {
+        ctx.scope.setOverride(1, 0);
+      });
+
+      it('should set override property', function() {
+        expect(ctx.scope.override.lines).to.be(true);
+      });
+
+      it('should update view model', function() {
+        expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
+        expect(ctx.scope.currentOverrides[0].value).to.be('true');
+      });
+    });
+
+    describe('When removing overide', function() {
+      it('click should include option and value index', function() {
+        ctx.scope.setOverride(1,0);
+        ctx.scope.removeOverride({ propertyName: 'lines' });
+        expect(ctx.scope.currentOverrides.length).to.be(0);
+      });
+    });
+
+  });
+
+});
+

+ 115 - 0
src/test/specs/timeSeries-specs.js

@@ -0,0 +1,115 @@
+define([
+  'components/timeSeries'
+], function(TimeSeries) {
+  'use strict';
+
+  describe("TimeSeries", function() {
+    var points, series;
+    var yAxisFormats = ['short', 'ms'];
+    var testData = {
+      info: { alias: 'test' },
+      datapoints: [
+        [1,2],[null,3],[10,4],[8,5]
+      ]
+    };
+
+    describe('when getting flot pairs', function() {
+      it('with connected style, should ignore nulls', function() {
+        series = new TimeSeries(testData);
+        points = series.getFlotPairs('connected', yAxisFormats);
+        expect(points.length).to.be(3);
+      });
+
+      it('with null as zero style, should replace nulls with zero', function() {
+        series = new TimeSeries(testData);
+        points = series.getFlotPairs('null as zero', yAxisFormats);
+        expect(points.length).to.be(4);
+        expect(points[1][1]).to.be(0);
+      });
+    });
+
+    describe('series overrides', function() {
+      var series;
+      beforeEach(function() {
+        series = new TimeSeries(testData);
+      });
+
+      describe('fill & points', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
+        });
+
+        it('should set fill zero, and enable points', function() {
+          expect(series.lines.fill).to.be(0.001);
+          expect(series.points.show).to.be(true);
+        });
+      });
+
+      describe('series option overrides, bars, true & lines false', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
+        });
+
+        it('should disable lines, and enable bars', function() {
+          expect(series.lines.show).to.be(false);
+          expect(series.bars.show).to.be(true);
+        });
+      });
+
+      describe('series option overrides, linewidth, stack', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
+        });
+
+        it('should disable stack, and set lineWidth', function() {
+          expect(series.stack).to.be(false);
+          expect(series.lines.lineWidth).to.be(5);
+        });
+      });
+
+      describe('series option overrides, pointradius, steppedLine', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
+        });
+
+        it('should set pointradius, and set steppedLine', function() {
+          expect(series.points.radius).to.be(5);
+          expect(series.lines.steps).to.be(true);
+        });
+      });
+
+      describe('override match on regex', function() {
+        beforeEach(function() {
+          series.info.alias = 'test_01';
+          series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
+        });
+
+        it('should match second series', function() {
+          expect(series.lines.show).to.be(false);
+        });
+      });
+
+      describe('override series y-axis, and z-index', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
+        });
+
+        it('should set yaxis', function() {
+          expect(series.info.yaxis).to.be(2);
+        });
+
+        it('should set zindex', function() {
+          expect(series.zindex).to.be(2);
+        });
+      });
+
+    });
+
+  });
+
+});

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

@@ -6,6 +6,7 @@ require.config({
     mocks:                 '../test/mocks',
     mocks:                 '../test/mocks',
     config:                '../config.sample',
     config:                '../config.sample',
     kbn:                   'components/kbn',
     kbn:                   'components/kbn',
+    store:                 'components/store',
 
 
     settings:              'components/settings',
     settings:              'components/settings',
     lodash:                'components/lodash.extended',
     lodash:                'components/lodash.extended',
@@ -26,7 +27,7 @@ require.config({
     crypto:                   '../vendor/crypto.min',
     crypto:                   '../vendor/crypto.min',
     spectrum:                 '../vendor/spectrum',
     spectrum:                 '../vendor/spectrum',
 
 
-    jquery:                   '../vendor/jquery/jquery-1.8.0',
+    jquery:                   '../vendor/jquery/jquery-2.1.1.min',
 
 
     bootstrap:                '../vendor/bootstrap/bootstrap',
     bootstrap:                '../vendor/bootstrap/bootstrap',
     'bootstrap-tagsinput':    '../vendor/tagsinput/bootstrap-tagsinput',
     'bootstrap-tagsinput':    '../vendor/tagsinput/bootstrap-tagsinput',
@@ -117,14 +118,18 @@ require([
     'specs/lexer-specs',
     'specs/lexer-specs',
     'specs/parser-specs',
     'specs/parser-specs',
     'specs/gfunc-specs',
     'specs/gfunc-specs',
+    'specs/timeSeries-specs',
     'specs/row-ctrl-specs',
     'specs/row-ctrl-specs',
     'specs/graphiteTargetCtrl-specs',
     'specs/graphiteTargetCtrl-specs',
     'specs/influxdb-datasource-specs',
     'specs/influxdb-datasource-specs',
     'specs/graph-ctrl-specs',
     'specs/graph-ctrl-specs',
+    'specs/grafanaGraph-specs',
+    'specs/seriesOverridesCtrl-specs',
     'specs/filterSrv-specs',
     'specs/filterSrv-specs',
     'specs/kbn-format-specs',
     'specs/kbn-format-specs',
     'specs/dashboardSrv-specs',
     'specs/dashboardSrv-specs',
     'specs/influxSeries-specs',
     'specs/influxSeries-specs',
+    'specs/dashboardViewStateSrv-specs',
     'specs/overview-ctrl-specs',
     'specs/overview-ctrl-specs',
   ], function () {
   ], function () {
     window.__karma__.start();
     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',
       require: '?jqyouiDroppable',
       restrict: 'A',
       restrict: 'A',
       link: function(scope, element, attrs) {
       link: function(scope, element, attrs) {
-        var dragSettings, zIndex;
+        // grafana change, remove watcher after first evaluation
+        var dragSettings, zIndex, removeWatcher;
         var updateDraggable = function(newValue, oldValue) {
         var updateDraggable = function(newValue, oldValue) {
           if (newValue) {
           if (newValue) {
+            removeWatcher();
             dragSettings = scope.$eval(element.attr('jqyoui-draggable')) || [];
             dragSettings = scope.$eval(element.attr('jqyoui-draggable')) || [];
             element
             element
               .draggable({disabled: false})
               .draggable({disabled: false})
@@ -283,7 +285,7 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
             element.draggable({disabled: true});
             element.draggable({disabled: true});
           }
           }
         };
         };
-        scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
+        removeWatcher = scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable);
         updateDraggable();
         updateDraggable();
       }
       }
     };
     };
@@ -292,8 +294,11 @@ var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$ti
       restrict: 'A',
       restrict: 'A',
       priority: 1,
       priority: 1,
       link: function(scope, element, attrs) {
       link: function(scope, element, attrs) {
+        // grafana change, remove watcher after first evaluation
+        var removeWatcher;
         var updateDroppable = function(newValue, oldValue) {
         var updateDroppable = function(newValue, oldValue) {
           if (newValue) {
           if (newValue) {
+            removeWatcher();
             element
             element
               .droppable({disabled: false})
               .droppable({disabled: false})
               .droppable(scope.$eval(attrs.jqyouiOptions) || {})
               .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();
         updateDroppable();
       }
       }
     };
     };

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

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

+ 3 - 2
src/vendor/bootstrap/bootstrap.js

@@ -753,7 +753,8 @@
 
 
     if (!selector) {
     if (!selector) {
       selector = $this.attr('href')
       selector = $this.attr('href')
-      selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+      // grafana backport fix from bootstrap 3
+      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
     }
     }
 
 
     $parent = selector && $(selector)
     $parent = selector && $(selector)
@@ -2319,4 +2320,4 @@
   })
   })
 
 
 
 
-}(window.jQuery);
+}(window.jQuery);

ファイルの差分が大きいため隠しています
+ 0 - 1
src/vendor/jquery/jquery-1.8.0.js


ファイルの差分が大きいため隠しています
+ 1 - 0
src/vendor/jquery/jquery-2.1.1.min.js


ファイルの差分が大きいため隠しています
+ 359 - 224
src/vendor/moment.js


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません