Просмотр исходного кода

Merge branch 'master' into panel_edit_menu_poc

Torkel Ödegaard 11 лет назад
Родитель
Сommit
6003fee33f
61 измененных файлов с 1019 добавлено и 254 удалено
  1. 15 1
      CHANGELOG.md
  2. 2 2
      latest.json
  3. 1 1
      package.json
  4. 15 4
      src/app/components/kbn.js
  5. 9 9
      src/app/controllers/dashboardCtrl.js
  6. 2 2
      src/app/controllers/dashboardNavCtrl.js
  7. 2 2
      src/app/controllers/graphiteTarget.js
  8. 14 2
      src/app/controllers/search.js
  9. 8 8
      src/app/dashboards/default.json
  10. 4 6
      src/app/dashboards/scripted_async.js
  11. 264 0
      src/app/dashboards/template_vars.json
  12. 17 9
      src/app/directives/grafanaGraph.js
  13. 1 1
      src/app/directives/graphiteSegment.js
  14. 2 2
      src/app/directives/tip.js
  15. 1 1
      src/app/filters/all.js
  16. 2 2
      src/app/panels/graph/axisEditor.html
  17. 1 1
      src/app/panels/graph/seriesOverridesCtrl.js
  18. 1 1
      src/app/panels/graph/styleEditor.html
  19. 2 3
      src/app/panels/text/editor.html
  20. 3 0
      src/app/partials/annotations_editor.html
  21. 4 1
      src/app/partials/dashboard_topnav.html
  22. 1 1
      src/app/partials/edit_json.html
  23. 94 16
      src/app/partials/graphite/editor.html
  24. 8 8
      src/app/partials/influxdb/editor.html
  25. 1 1
      src/app/partials/opentsdb/editor.html
  26. 0 6
      src/app/partials/paneladd.html
  27. 1 1
      src/app/partials/playlist.html
  28. 1 1
      src/app/partials/search.html
  29. 2 1
      src/app/services/annotationsSrv.js
  30. 1 1
      src/app/services/dashboard/dashboardKeyBindings.js
  31. 1 1
      src/app/services/dashboard/dashboardSrv.js
  32. 32 23
      src/app/services/dashboard/dashboardViewStateSrv.js
  33. 1 1
      src/app/services/datasourceSrv.js
  34. 99 35
      src/app/services/elasticsearch/es-datasource.js
  35. 15 1
      src/app/services/graphite/gfunc.js
  36. 60 17
      src/app/services/influxdb/influxdbDatasource.js
  37. 1 1
      src/app/services/opentsdb/opentsdbDatasource.js
  38. 34 22
      src/app/services/templateSrv.js
  39. 17 11
      src/app/services/templateValuesSrv.js
  40. 33 3
      src/app/services/timeSrv.js
  41. 8 0
      src/app/services/unsavedChangesSrv.js
  42. 5 6
      src/config.sample.js
  43. 1 1
      src/css/less/bootswatch.dark.less
  44. 1 1
      src/css/less/bootswatch.light.less
  45. 44 6
      src/css/less/grafana.less
  46. 36 6
      src/css/less/overrides.less
  47. 1 1
      src/css/less/variables.dark.less
  48. BIN
      src/img/fav16.png
  49. BIN
      src/img/fav32.png
  50. BIN
      src/img/fav_dark_16.png
  51. BIN
      src/img/fav_dark_32.png
  52. 13 6
      src/index.html
  53. 2 0
      src/test/specs/dashboardViewStateSrv-specs.js
  54. 19 0
      src/test/specs/graphiteTargetCtrl-specs.js
  55. 2 0
      src/test/specs/helpers.js
  56. 25 1
      src/test/specs/influxdb-datasource-specs.js
  57. 6 0
      src/test/specs/kbn-format-specs.js
  58. 35 0
      src/test/specs/templateSrv-specs.js
  59. 3 15
      src/test/specs/templateValuesSrv-specs.js
  60. 40 1
      src/test/specs/timeSrv-specs.js
  61. 6 1
      tasks/options/requirejs.js

+ 15 - 1
CHANGELOG.md

@@ -1,5 +1,14 @@
 # 1.8.0 (unreleased)
 # 1.8.0 (unreleased)
 
 
+**Fixes**
+- [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource
+- [Issue #795](https://github.com/grafana/grafana/issues/795). Chrome: Fix for display issue in chrome beta & chrome canary when entering edit mode
+- [Issue #818](https://github.com/grafana/grafana/issues/818). Graph: Added percent y-axis format
+- [Issue #828](https://github.com/grafana/grafana/issues/828). Elasticsearch: saving new dashboard with title equal to slugified url would cause it to deleted.
+- [Issue #830](https://github.com/grafana/grafana/issues/830). Annotations: Fix for elasticsearch annotations and mapping nested fields
+
+# 1.8.0-RC1 (2014-09-12)
+
 **UI polish / changes**
 **UI polish / changes**
 - [Issue #725](https://github.com/grafana/grafana/issues/725). UI: All modal editors are removed and replaced by an edit pane under menu. The look of editors is also updated and polished. Search dropdown is also shown as pane under menu and has seen some UI polish.
 - [Issue #725](https://github.com/grafana/grafana/issues/725). UI: All modal editors are removed and replaced by an edit pane under menu. The look of editors is also updated and polished. Search dropdown is also shown as pane under menu and has seen some UI polish.
 
 
@@ -13,6 +22,8 @@
 - [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
 - [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
 - [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
 - [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
 - [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
 - [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
+- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname.
+- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details.
 
 
 **InfluxDB Breaking changes**
 **InfluxDB Breaking changes**
 - To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
 - To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
@@ -41,6 +52,9 @@
 - [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)
 - [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)
 - [Issue #634](https://github.com/grafana/grafana/issues/634). Dashboard: Dashboard tags now in different colors (from fixed palette) determined by tag name.
 - [Issue #634](https://github.com/grafana/grafana/issues/634). Dashboard: Dashboard tags now in different colors (from fixed palette) determined by tag name.
 - [Issue #685](https://github.com/grafana/grafana/issues/685). Dashboard: New config.js option to change/remove window title prefix.
 - [Issue #685](https://github.com/grafana/grafana/issues/685). Dashboard: New config.js option to change/remove window title prefix.
+- [Issue #781](https://github.com/grafana/grafana/issues/781). Dashboard: Title URL is now slugified for greater URL readability, works with both ES & InfluxDB storage, is backward compatible
+- [Issue #785](https://github.com/grafana/grafana/issues/785). Elasticsearch: Support for full elasticsearch lucene search grammar when searching for dashboards, better async search
+- [Issue #787](https://github.com/grafana/grafana/issues/787). Dashboard: time range can now be read from URL parameters, will override dashboard saved time range
 
 
 **Fixes**
 **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 #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)
@@ -234,7 +248,7 @@ Read this for more info:
 - More graphite function definitions
 - More graphite function definitions
 - Make "ms" axis format include hour, day, weeks, month and year ([Issue #149](https://github.com/grafana/grafana/issues/149))
 - Make "ms" axis format include hour, day, weeks, month and year ([Issue #149](https://github.com/grafana/grafana/issues/149))
 - Microsecond axis format ([Issue #146](https://github.com/grafana/grafana/issues/146))
 - Microsecond axis format ([Issue #146](https://github.com/grafana/grafana/issues/146))
-- Specify template paramaters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123))
+- Specify template parameters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123))
 
 
 ### Fixes
 ### Fixes
 - Basic Auth fix ([Issue #152](https://github.com/grafana/grafana/issues/152))
 - Basic Auth fix ([Issue #152](https://github.com/grafana/grafana/issues/152))

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-	"version": "1.7.0",
-	"url": "http://grafanarel.s3.amazonaws.com/grafana-1.7.0"
+	"version": "1.8.0-rc1",
+	"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.0-rc1"
 }
 }

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
     "company": "Coding Instinct AB"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "1.8.0",
+  "version": "1.8.0-rc1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/torkelo/grafana.git"
     "url": "http://github.com/torkelo/grafana.git"

+ 15 - 4
src/app/components/kbn.js

@@ -531,6 +531,10 @@ function($, _, moment) {
       return function(val) {
       return function(val) {
         return kbn.nanosFormat(val, decimals);
         return kbn.nanosFormat(val, decimals);
       };
       };
+    case 'percent':
+      return function(val, axis) {
+        return kbn.noneFormat(val, axis ? axis.tickDecimals : null) + ' %';
+      };
     default:
     default:
       return function(val, axis) {
       return function(val, axis) {
         return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
         return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
@@ -563,8 +567,8 @@ function($, _, moment) {
 
 
   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) {
-      return kbn.microsFormat(size * 1000,decimals);
+    if (size !== 0 && Math.abs(size) < 1) {
+      return kbn.microsFormat(size * 1000, decimals);
     }
     }
     else if (Math.abs(size) < 1000) {
     else if (Math.abs(size) < 1000) {
       return size.toFixed(decimals) + " ms";
       return size.toFixed(decimals) + " ms";
@@ -591,7 +595,7 @@ function($, _, moment) {
 
 
   kbn.sFormat = function(size, decimals) {
   kbn.sFormat = function(size, decimals) {
     // Less than 1 sec, downscale to milli
     // Less than 1 sec, downscale to milli
-    if (Math.abs(size) < 1) {
+    if (size !== 0 && Math.abs(size) < 1) {
       return kbn.msFormat(size * 1000, decimals);
       return kbn.msFormat(size * 1000, decimals);
     }
     }
     // Less than 10 min, use seconds
     // Less than 10 min, use seconds
@@ -620,7 +624,7 @@ function($, _, moment) {
 
 
   kbn.microsFormat = function(size, decimals) {
   kbn.microsFormat = function(size, decimals) {
     // Less than 1 micro, downscale to nano
     // Less than 1 micro, downscale to nano
-    if (Math.abs(size) < 1) {
+    if (size !== 0 && Math.abs(size) < 1) {
       return kbn.nanosFormat(size * 1000, decimals);
       return kbn.nanosFormat(size * 1000, decimals);
     }
     }
     else if (Math.abs(size) < 1000) {
     else if (Math.abs(size) < 1000) {
@@ -655,6 +659,13 @@ function($, _, moment) {
     }
     }
   };
   };
 
 
+  kbn.slugifyForUrl = function(str) {
+    return str
+      .toLowerCase()
+      .replace(/[^\w ]+/g,'')
+      .replace(/ +/g,'-');
+  };
+
   kbn.stringToJsRegex = function(str) {
   kbn.stringToJsRegex = function(str) {
     if (str[0] !== '/') {
     if (str[0] !== '/') {
       return new RegExp(str);
       return new RegExp(str);

+ 9 - 9
src/app/controllers/dashboardCtrl.js

@@ -44,14 +44,14 @@ function (angular, $, config, _) {
     $scope.setupDashboard = function(event, dashboardData) {
     $scope.setupDashboard = function(event, dashboardData) {
       $rootScope.performance.dashboardLoadStart = new Date().getTime();
       $rootScope.performance.dashboardLoadStart = new Date().getTime();
       $rootScope.performance.panelsInitialized = 0;
       $rootScope.performance.panelsInitialized = 0;
-      $rootScope.performance.panelsRendered= 0;
+      $rootScope.performance.panelsRendered = 0;
 
 
       $scope.dashboard = dashboardSrv.create(dashboardData);
       $scope.dashboard = dashboardSrv.create(dashboardData);
       $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
       $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
 
 
       // init services
       // init services
       timeSrv.init($scope.dashboard);
       timeSrv.init($scope.dashboard);
-      templateValuesSrv.init($scope.dashboard);
+      templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
       panelMoveSrv.init($scope.dashboard, $scope);
       panelMoveSrv.init($scope.dashboard, $scope);
 
 
       $scope.checkFeatureToggles();
       $scope.checkFeatureToggles();
@@ -93,18 +93,18 @@ function (angular, $, config, _) {
       };
       };
     };
     };
 
 
-    $scope.panel_path =function(type) {
-      if(type) {
-        return 'app/panels/'+type.replace(".","/");
+    $scope.edit_path = function(type) {
+      var p = $scope.panel_path(type);
+      if(p) {
+        return p+'/editor.html';
       } else {
       } else {
         return false;
         return false;
       }
       }
     };
     };
 
 
-    $scope.edit_path = function(type) {
-      var p = $scope.panel_path(type);
-      if(p) {
-        return p+'/editor.html';
+    $scope.panel_path =function(type) {
+      if(type) {
+        return 'app/panels/'+type.replace(".","/");
       } else {
       } else {
         return false;
         return false;
       }
       }

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

@@ -78,7 +78,7 @@ function (angular, _, moment, config, store) {
       var clone = angular.copy($scope.dashboard);
       var clone = angular.copy($scope.dashboard);
       $scope.db.saveDashboard(clone)
       $scope.db.saveDashboard(clone)
         .then(function(result) {
         .then(function(result) {
-          alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
+          alertSrv.set('Dashboard Saved', 'Saved as "' + result.title + '"','success', 3000);
 
 
           if (result.url !== $location.path()) {
           if (result.url !== $location.path()) {
             $location.search({});
             $location.search({});
@@ -88,7 +88,7 @@ function (angular, _, moment, config, store) {
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
 
 
         }, function(err) {
         }, function(err) {
-          alertSrv.set('Save failed', err, 'error',5000);
+          alertSrv.set('Save failed', err, 'error', 5000);
         });
         });
     };
     };
 
 

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

@@ -15,7 +15,7 @@ function (angular, _, config, gfunc, Parser) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.target.target = $scope.target.target || '';
       $scope.target.target = $scope.target.target || '';
-      $scope.targetLetter = targetLetters[$scope.$index];
+      $scope.targetLetters = targetLetters;
 
 
       parseTarget();
       parseTarget();
     };
     };
@@ -90,7 +90,7 @@ function (angular, _, config, gfunc, Parser) {
         break;
         break;
       case 'metric':
       case 'metric':
         if ($scope.segments.length > 0) {
         if ($scope.segments.length > 0) {
-          if ($scope.segments[0].length !== 1) {
+          if (astNode.segments.length !== 1) {
             throw { message: 'Multiple metric params not supported, use text editor.' };
             throw { message: 'Multiple metric params not supported, use text editor.' };
           }
           }
           addFunctionParameter(func, astNode.segments[0].value, index, true);
           addFunctionParameter(func, astNode.segments[0].value, index, true);

+ 14 - 2
src/app/controllers/search.js

@@ -17,6 +17,7 @@ function (angular, _, config, $) {
       $scope.results = {dashboards: [], tags: [], metrics: []};
       $scope.results = {dashboards: [], tags: [], metrics: []};
       $scope.query = { query: 'title:' };
       $scope.query = { query: 'title:' };
       $scope.db = datasourceSrv.getGrafanaDB();
       $scope.db = datasourceSrv.getGrafanaDB();
+      $scope.currentSearchId = 0;
 
 
       $timeout(function() {
       $timeout(function() {
         $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
         $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
@@ -75,8 +76,18 @@ function (angular, _, config, $) {
     };
     };
 
 
     $scope.searchDashboards = function(queryString) {
     $scope.searchDashboards = function(queryString) {
+      // bookeeping for determining stale search requests
+      var searchId = $scope.currentSearchId + 1;
+      $scope.currentSearchId = searchId > $scope.currentSearchId ? searchId : $scope.currentSearchId;
+
       return $scope.db.searchDashboards(queryString)
       return $scope.db.searchDashboards(queryString)
         .then(function(results) {
         .then(function(results) {
+          // since searches are async, it's possible that these results are not for the latest search. throw
+          // them away if so
+          if (searchId < $scope.currentSearchId) {
+            return;
+          }
+
           $scope.tagsOnly = results.tagsOnly;
           $scope.tagsOnly = results.tagsOnly;
           $scope.results.dashboards = results.dashboards;
           $scope.results.dashboards = results.dashboards;
           $scope.results.tags = results.tags;
           $scope.results.tags = results.tags;
@@ -108,9 +119,10 @@ function (angular, _, config, $) {
       $scope.searchDashboards($scope.query.query);
       $scope.searchDashboards($scope.query.query);
     };
     };
 
 
-    $scope.deleteDashboard = function(id, evt) {
+    $scope.deleteDashboard = function(dash, evt) {
       evt.stopPropagation();
       evt.stopPropagation();
-      $scope.emitAppEvent('delete-dashboard', { id: id });
+      $scope.emitAppEvent('delete-dashboard', { id: dash.id });
+      $scope.results.dashboards = _.without($scope.results.dashboards, dash);
     };
     };
 
 
     $scope.addMetricToCurrentDashboard = function (metricId) {
     $scope.addMetricToCurrentDashboard = function (metricId) {

+ 8 - 8
src/app/dashboards/default.json

@@ -9,14 +9,15 @@
       "title": "New row",
       "title": "New row",
       "height": "150px",
       "height": "150px",
       "collapse": false,
       "collapse": false,
+      "editable": true,
       "panels": [
       "panels": [
         {
         {
-          "error": false,
+          "id": 1,
           "span": 12,
           "span": 12,
           "editable": true,
           "editable": true,
           "type": "text",
           "type": "text",
           "mode": "html",
           "mode": "html",
-          "content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"http://grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
+          "content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"//grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
           "style": {},
           "style": {},
           "title": "Welcome to"
           "title": "Welcome to"
         }
         }
@@ -26,22 +27,20 @@
       "title": "Welcome to Grafana",
       "title": "Welcome to Grafana",
       "height": "210px",
       "height": "210px",
       "collapse": false,
       "collapse": false,
+      "editable": true,
       "panels": [
       "panels": [
         {
         {
-          "error": false,
+          "id": 2,
           "span": 6,
           "span": 6,
-          "editable": true,
           "type": "text",
           "type": "text",
-          "loadingEditor": false,
           "mode": "html",
           "mode": "html",
           "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a>  (Must read!)\n      </li>\n    </ul>\n  </div>\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n      </li>\n    </ul>\n  </div>\n</div>",
           "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a>  (Must read!)\n      </li>\n    </ul>\n  </div>\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n      </li>\n    </ul>\n  </div>\n</div>",
           "style": {},
           "style": {},
           "title": "Documentation Links"
           "title": "Documentation Links"
         },
         },
         {
         {
-          "error": false,
+          "id": 3,
           "span": 6,
           "span": 6,
-          "editable": true,
           "type": "text",
           "type": "text",
           "mode": "html",
           "mode": "html",
           "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span12\">\n    <ul>\n      <li>Ctrl+S saves the current dashboard</li>\n      <li>Ctrl+F Opens the dashboard finder</li>\n      <li>Ctrl+H Hide/show row controls</li>\n      <li>Click and drag graph title to move panel</li>\n      <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n      <li>Click the colored icon in the legend to change series color</li>\n      <li>Ctrl or Shift + Click legend name to hide other series</li>\n    </ul>\n  </div>\n</div>\n",
           "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span12\">\n    <ul>\n      <li>Ctrl+S saves the current dashboard</li>\n      <li>Ctrl+F Opens the dashboard finder</li>\n      <li>Ctrl+H Hide/show row controls</li>\n      <li>Click and drag graph title to move panel</li>\n      <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n      <li>Click the colored icon in the legend to change series color</li>\n      <li>Ctrl or Shift + Click legend name to hide other series</li>\n    </ul>\n  </div>\n</div>\n",
@@ -53,11 +52,12 @@
     {
     {
       "title": "test",
       "title": "test",
       "height": "250px",
       "height": "250px",
+      "editable": true,
       "collapse": false,
       "collapse": false,
       "panels": [
       "panels": [
         {
         {
+          "id": 4,
           "span": 12,
           "span": 12,
-          "editable": true,
           "type": "graph",
           "type": "graph",
           "x-axis": true,
           "x-axis": true,
           "y-axis": true,
           "y-axis": true,

+ 4 - 6
src/app/dashboards/scripted_async.js

@@ -35,11 +35,9 @@ return function(callback) {
 
 
   // Set a title
   // Set a title
   dashboard.title = 'Scripted dash';
   dashboard.title = 'Scripted dash';
-  dashboard.services.filter = {
-    time: {
-      from: "now-" + (ARGS.from || timspan),
-      to: "now"
-    }
+  dashboard.time = {
+    from: "now-" + (ARGS.from || timspan),
+    to: "now"
   };
   };
 
 
   var rows = 1;
   var rows = 1;
@@ -78,4 +76,4 @@ return function(callback) {
     callback(dashboard);
     callback(dashboard);
 
 
   });
   });
-}
+}

+ 264 - 0
src/app/dashboards/template_vars.json

@@ -0,0 +1,264 @@
+{
+  "id": null,
+  "title": "Templated Graphs Nested",
+  "originalTitle": "Templated Graphs Nested",
+  "tags": [
+    "showcase",
+    "templated"
+  ],
+  "style": "dark",
+  "timezone": "browser",
+  "editable": true,
+  "hideControls": false,
+  "rows": [
+    {
+      "title": "Row1",
+      "height": "350px",
+      "editable": true,
+      "collapse": false,
+      "collapsable": true,
+      "panels": [
+        {
+          "span": 12,
+          "editable": true,
+          "type": "graph",
+          "loadingEditor": false,
+          "datasource": null,
+          "renderer": "flot",
+          "x-axis": true,
+          "y-axis": true,
+          "scale": 1,
+          "y_formats": [
+            "short",
+            "short"
+          ],
+          "grid": {
+            "max": null,
+            "min": 0,
+            "threshold1": null,
+            "threshold2": null,
+            "threshold1Color": "rgba(216, 200, 27, 0.27)",
+            "threshold2Color": "rgba(234, 112, 112, 0.22)",
+            "leftMax": null,
+            "rightMax": null,
+            "leftMin": null,
+            "rightMin": null
+          },
+          "annotate": {
+            "enable": false
+          },
+          "resolution": 100,
+          "lines": true,
+          "fill": 1,
+          "linewidth": 1,
+          "points": false,
+          "pointradius": 5,
+          "bars": false,
+          "stack": true,
+          "legend": {
+            "show": true,
+            "values": false,
+            "min": false,
+            "max": false,
+            "current": false,
+            "total": false,
+            "avg": false
+          },
+          "percentage": false,
+          "zerofill": true,
+          "nullPointMode": "connected",
+          "steppedLine": false,
+          "tooltip": {
+            "value_type": "cumulative",
+            "query_as_alias": true
+          },
+          "targets": [
+            {
+              "target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)",
+              "function": "mean",
+              "column": "value"
+            }
+          ],
+          "aliasColors": {
+            "highres.test": "#1F78C1",
+            "scale(highres.test,3)": "#6ED0E0",
+            "mobile": "#6ED0E0",
+            "tablet": "#EAB839"
+          },
+          "title": "Traffic [[period]]",
+          "id": 1,
+          "seriesOverrides": []
+        }
+      ],
+      "notice": false
+    },
+    {
+      "title": "Row1",
+      "height": "350px",
+      "editable": true,
+      "collapse": false,
+      "collapsable": true,
+      "panels": [
+        {
+          "span": 12,
+          "editable": true,
+          "type": "graph",
+          "loadingEditor": false,
+          "datasource": null,
+          "renderer": "flot",
+          "x-axis": true,
+          "y-axis": true,
+          "scale": 1,
+          "y_formats": [
+            "short",
+            "short"
+          ],
+          "grid": {
+            "max": null,
+            "min": 0,
+            "threshold1": null,
+            "threshold2": null,
+            "threshold1Color": "rgba(216, 200, 27, 0.27)",
+            "threshold2Color": "rgba(234, 112, 112, 0.22)",
+            "leftMax": null,
+            "rightMax": null,
+            "leftMin": null,
+            "rightMin": null
+          },
+          "annotate": {
+            "enable": false
+          },
+          "resolution": 100,
+          "lines": true,
+          "fill": 1,
+          "linewidth": 1,
+          "points": false,
+          "pointradius": 5,
+          "bars": false,
+          "stack": true,
+          "legend": {
+            "show": true,
+            "values": false,
+            "min": false,
+            "max": false,
+            "current": false,
+            "total": false,
+            "avg": false
+          },
+          "percentage": false,
+          "zerofill": true,
+          "nullPointMode": "connected",
+          "steppedLine": false,
+          "tooltip": {
+            "value_type": "cumulative",
+            "query_as_alias": true
+          },
+          "targets": [
+            {
+              "target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)"
+            }
+          ],
+          "aliasColors": {
+            "highres.test": "#1F78C1",
+            "scale(highres.test,3)": "#6ED0E0",
+            "mobile": "#6ED0E0",
+            "tablet": "#EAB839"
+          },
+          "title": "Second pannel",
+          "id": 2,
+          "seriesOverrides": []
+        }
+      ],
+      "notice": false
+    }
+  ],
+  "nav": [
+    {
+      "type": "timepicker",
+      "collapse": false,
+      "notice": false,
+      "enable": true,
+      "status": "Stable",
+      "time_options": [
+        "5m",
+        "15m",
+        "1h",
+        "6h",
+        "12h",
+        "24h",
+        "2d",
+        "7d",
+        "30d"
+      ],
+      "refresh_intervals": [
+        "5s",
+        "10s",
+        "30s",
+        "1m",
+        "5m",
+        "15m",
+        "30m",
+        "1h",
+        "2h",
+        "1d"
+      ],
+      "now": true
+    }
+  ],
+  "time": {
+    "from": "now-15m",
+    "to": "now"
+  },
+  "templating": {
+    "list": [
+      {
+        "type": "query",
+        "name": "app",
+        "query": "apps.*",
+        "includeAll": true,
+        "options": [],
+        "current": {
+          "text": "All",
+          "value": "*"
+        },
+        "datasource": null,
+        "allFormat": "wildcard",
+        "refresh": true
+      },
+      {
+        "type": "query",
+        "name": "server",
+        "query": "apps.$app.*",
+        "includeAll": true,
+        "options": [],
+        "current": {
+          "text": "All",
+          "value": "*"
+        },
+        "datasource": null,
+        "allFormat": "Glob",
+        "refresh": false
+      },
+      {
+        "type": "query",
+        "datasource": null,
+        "refresh_on_load": false,
+        "name": "metric",
+        "options": [],
+        "includeAll": true,
+        "allFormat": "glob",
+        "query": "apps.$app.$server.*",
+        "current": {
+          "text": "counters",
+          "value": "counters"
+        }
+      }
+    ],
+    "enable": true
+  },
+  "annotations": {
+    "enable": false
+  },
+  "refresh": false,
+  "version": 6
+}

+ 17 - 9
src/app/directives/grafanaGraph.js

@@ -15,7 +15,7 @@ function (angular, $, kbn, moment, _) {
       restrict: 'A',
       restrict: 'A',
       template: '<div> </div>',
       template: '<div> </div>',
       link: function(scope, elem) {
       link: function(scope, elem) {
-        var data, plot, annotations;
+        var data, annotations;
         var hiddenData = {};
         var hiddenData = {};
         var dashboard = scope.dashboard;
         var dashboard = scope.dashboard;
         var legendSideLastValue = null;
         var legendSideLastValue = null;
@@ -82,6 +82,10 @@ function (angular, $, kbn, moment, _) {
             render_panel_as_graphite_png(data);
             render_panel_as_graphite_png(data);
             return true;
             return true;
           }
           }
+
+          if (elem.width() === 0) {
+            return;
+          }
         }
         }
 
 
         // Function for rendering panel
         // Function for rendering panel
@@ -165,18 +169,22 @@ function (angular, $, kbn, moment, _) {
 
 
           var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
           var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
 
 
-          // if legend is to the right delay plot draw a few milliseconds
-          // so the legend width calculation can be done
+          function callPlot() {
+            try {
+              $.plot(elem, sortedSeries, options);
+            } catch (e) {
+              console.log('flotcharts error', e);
+            }
+
+            addAxisLabels();
+          }
+
           if (shouldDelayDraw(panel)) {
           if (shouldDelayDraw(panel)) {
+            setTimeout(callPlot, 50);
             legendSideLastValue = panel.legend.rightSide;
             legendSideLastValue = panel.legend.rightSide;
-            setTimeout(function() {
-              plot = $.plot(elem, sortedSeries, options);
-              addAxisLabels();
-            }, 50);
           }
           }
           else {
           else {
-            plot = $.plot(elem, sortedSeries, options);
-            addAxisLabels();
+            callPlot();
           }
           }
         }
         }
 
 

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

@@ -94,7 +94,7 @@ function (angular, app, _, $) {
           };
           };
 
 
           $input.attr('data-provide', 'typeahead');
           $input.attr('data-provide', 'typeahead');
-          $input.typeahead({ source: $scope.source, minLength: 0, items: 100, updater: $scope.updater });
+          $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater });
 
 
           var typeahead = $input.data('typeahead');
           var typeahead = $input.data('typeahead');
           typeahead.lookup = function () {
           typeahead.lookup = function () {

+ 2 - 2
src/app/directives/tip.js

@@ -11,10 +11,10 @@ function (angular, kbn) {
       return {
       return {
         restrict: 'E',
         restrict: 'E',
         link: function(scope, elem, attrs) {
         link: function(scope, elem, attrs) {
-          var _t = '<i class="icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
+          var _t = '<i class="grafana-tip icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
             kbn.addslashes(elem.text())+'\'"></i>';
             kbn.addslashes(elem.text())+'\'"></i>';
           elem.replaceWith($compile(angular.element(_t))(scope));
           elem.replaceWith($compile(angular.element(_t))(scope));
         }
         }
       };
       };
     });
     });
-});
+});

+ 1 - 1
src/app/filters/all.js

@@ -57,7 +57,7 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
 
 
   module.filter('interpolateTemplateVars', function(templateSrv) {
   module.filter('interpolateTemplateVars', function(templateSrv) {
     return function(text) {
     return function(text) {
-      return templateSrv.replace(text);
+      return templateSrv.replaceWithText(text);
     };
     };
   });
   });
 
 

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

@@ -4,7 +4,7 @@
     <h5>Left Y Axis</h5>
     <h5>Left Y Axis</h5>
        <div class="editor-option">
        <div class="editor-option">
         <label class="small">Format <tip>Y-axis formatting</tip></label>
         <label class="small">Format <tip>Y-axis formatting</tip></label>
-        <select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
+        <select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
       </div>
       </div>
       <div class="editor-option">
       <div class="editor-option">
         <label class="small">Min / <a ng-click="toggleGridMinMax('leftMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.leftMin)"></i></a></label>
         <label class="small">Min / <a ng-click="toggleGridMinMax('leftMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.leftMin)"></i></a></label>
@@ -23,7 +23,7 @@
     <h5>Right Y Axis</h5>
     <h5>Right Y Axis</h5>
        <div class="editor-option">
        <div class="editor-option">
         <label class="small">Format <tip>Y-axis formatting</tip></label>
         <label class="small">Format <tip>Y-axis formatting</tip></label>
-        <select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
+        <select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
       </div>
       </div>
       <div class="editor-option">
       <div class="editor-option">
         <label class="small">Min / <a ng-click="toggleGridMinMax('rightMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.rightMin)"></i></a></label>
         <label class="small">Min / <a ng-click="toggleGridMinMax('rightMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.rightMin)"></i></a></label>

+ 1 - 1
src/app/panels/graph/seriesOverridesCtrl.js

@@ -70,7 +70,7 @@ define([
     $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
     $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
     $scope.addOverrideOption('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
-    $scope.addOverrideOption('Stack', 'stack', [true, false]);
+    $scope.addOverrideOption('Stack', 'stack', [true, false, 2, 3, 4, 5]);
     $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
     $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
     $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
     $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
     $scope.updateCurrentOverrides();
     $scope.updateCurrentOverrides();

+ 1 - 1
src/app/panels/graph/styleEditor.html

@@ -27,7 +27,7 @@
       <select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
       <select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
     </div>
     </div>
     <div class="editor-option">
     <div class="editor-option">
-      <label class="small">Null point mode <tip>Define how null values should be drawn</tip></label>
+      <label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
       <select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
       <select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
     </div>
     </div>
     <div class="editor-option">
     <div class="editor-option">

+ 2 - 3
src/app/panels/text/editor.html

@@ -9,10 +9,9 @@
   </div>
   </div>
 
 
   <label class=small>Content
   <label class=small>Content
-    <span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
     <span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
     <span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
   </label>
   </label>
 
 
-  <textarea ng-model="panel.content" rows="6" style="width:95%" ng-change="render()" ng-model-onblur>
+  <textarea ng-model="panel.content" rows="20" style="width:95%" ng-change="render()" ng-model-onblur>
   </textarea>
   </textarea>
-</div>
+</div>

+ 3 - 0
src/app/partials/annotations_editor.html

@@ -16,6 +16,9 @@
 	<div class="dashboard-editor-body">
 	<div class="dashboard-editor-body">
 		<div class="editor-row row" ng-if="editor.index == 0">
 		<div class="editor-row row" ng-if="editor.index == 0">
 			<div class="span6">
 			<div class="span6">
+				<div ng-if="variables.length === 0">
+					<em>No annotations defined</em>
+				</div>
 				<table class="grafana-options-table">
 				<table class="grafana-options-table">
 					<tr ng-repeat="annotation in annotations">
 					<tr ng-repeat="annotation in annotations">
 						<td style="width:90%">
 						<td style="width:90%">

+ 4 - 1
src/app/partials/dashboard_topnav.html

@@ -1,7 +1,10 @@
 <div class="navbar navbar-static-top">
 <div class="navbar navbar-static-top">
 	<div class="navbar-inner">
 	<div class="navbar-inner">
 		<div class="container-fluid">
 		<div class="container-fluid">
-			<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
+			<span class="brand">
+				<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
+				<span class="page-title">{{dashboard.title}}</span>
+			</span>
 			<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
 			<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
 
 
 				<li ng-show="dashboardViewState.fullscreen">
 				<li ng-show="dashboardViewState.fullscreen">

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

@@ -8,7 +8,7 @@
 	</div>
 	</div>
 
 
 	<div class="dashboard-editor-body" style="height: 500px">
 	<div class="dashboard-editor-body" style="height: 500px">
-		<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%; color: white"></textarea>
+		<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%;"></textarea>
 	</div>
 	</div>
 
 
 	<div class="dashboard-editor-footer">
 	<div class="dashboard-editor-footer">

+ 94 - 16
src/app/partials/graphite/editor.html

@@ -1,4 +1,4 @@
-<div class="editor-row" style="margin-top: 10px;">
+<div class="editor-row">
 
 
 	<div  ng-repeat="target in panel.targets"
 	<div  ng-repeat="target in panel.targets"
         class="grafana-target"
         class="grafana-target"
@@ -30,13 +30,6 @@
                   ng-click="duplicate()">
                   ng-click="duplicate()">
                 Duplicate
                 Duplicate
               </a>
               </a>
-            </li>
-            <li role="menuitem">
-              <a  tabindex="1"
-                  ng-click="toggleMetricOptions()">
-                Toggle request options
-              </a>
-            </li>
           </ul>
           </ul>
         </li>
         </li>
         <li>
         <li>
@@ -48,7 +41,7 @@
 
 
       <ul class="grafana-segment-list">
       <ul class="grafana-segment-list">
 				<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
 				<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
-					{{targetLetter}}
+					{{targetLetters[$index]}}
         </li>
         </li>
         <li>
         <li>
           <a  class="grafana-target-segment"
           <a  class="grafana-target-segment"
@@ -80,24 +73,109 @@
     </div>
     </div>
   </div>
   </div>
 
 
-	<div class="grafana-target grafana-metric-options" ng-if="panel.metricOptionsEnabled">
+</div>
+
+<section class="grafana-metric-options">
 		<div class="grafana-target-inner">
 		<div class="grafana-target-inner">
 			<ul class="grafana-segment-list">
 			<ul class="grafana-segment-list">
+				<li class="grafana-target-segment grafana-target-segment-icon">
+					<i class="icon-wrench"></i>
+				</li>
 				<li class="grafana-target-segment">
 				<li class="grafana-target-segment">
-					cacheTimeout <tip>Graphite parameter to overwride memcache default timeout (unit is seconds)</tip>
+					cacheTimeout
 				</li>
 				</li>
 				<li>
 				<li>
 					<input type="text"
 					<input type="text"
-								class="input-large grafana-target-segment-input"
-								ng-model="panel.cacheTimeout"
-								spellcheck='false'
-								placeholder="60">
+					class="input-mini grafana-target-segment-input"
+					ng-model="panel.cacheTimeout"
+					bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'"
+					data-placement="right"
+					spellcheck='false'
+					placeholder="60">
 				</li>
 				</li>
 			</ul>
 			</ul>
-      <div class="clearfix"></div>
+			<div class="clearfix"></div>
+		</div>
+		<div class="grafana-target-inner">
+			<ul class="grafana-segment-list">
+				<li class="grafana-target-segment grafana-target-segment-icon">
+					<i class="icon-info-sign"></i>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						shorter legend names
+					</a>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						series as parameters
+					</a>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						stacking
+					</a>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						templating
+					</a>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
 		</div>
 		</div>
 	</div>
 	</div>
+</section>
+
+<div class="editor-row">
+	<div class="pull-left" style="margin-top: 30px;">
+
+		<div class="grafana-info-box span8" ng-if="editorHelpIndex === 1">
+			<h5>Shorter legend names</h5>
+			<ul>
+				<li>alias() function to specify a custom series name</li>
+				<li>aliasByNode(2) to alias by a specific part of your metric path</li>
+				<li>aliasByNode(2, -1) you can add multiple segment paths, and use negative index</li>
+				<li>groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by</li>
+			</ul>
+		</div>
+
+		<div class="grafana-info-box span8" ng-if="editorHelpIndex === 2">
+			<h5>Series as parameter</h5>
+			<ul>
+				<li>Some graphite functions allow you to have many series arguments</li>
+				<li>Use #[A-Z] to use a graphite query as parameter to a function</li>
+				<li>
+					Examples:
+					<ul>
+						<li>asPercent(#A, #B)</li>
+						<li>prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query</li>
+						<li>prod.srv-01.counters.count - sumSeries(#A) : sum count and series A </li>
+						<li>divideSeries(#A, #B)</li>
+					</ul>
+				</li>
+				<li>If a query is added only to be used as a parameter, hide it from the graph with the eye icon</li>
+			</ul>
+		</div>
+
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
+			<h5>Stacking</h5>
+			<ul>
+				<li>You find the stacking option under Display Styles tab</li>
+				<li>When stacking is enabled make sure null point mode is set to 'null as zero'</li>
+			</ul>
+		</div>
 
 
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 4">
+			<h5>Templating</h5>
+			<ul>
+				<li>You can use a template variable in place of metric names</li>
+				<li>You can use a template variable in place of function parameters</li>
+				<li>You enable the templating feature in Dashboard settings / Feature toggles </li>
+			</ul>
+		</div>
+
+	</div>
 </div>
 </div>
 
 
 
 

+ 8 - 8
src/app/partials/influxdb/editor.html

@@ -181,8 +181,8 @@
 	<div class="grafana-target">
 	<div class="grafana-target">
 		<div class="grafana-target-inner">
 		<div class="grafana-target-inner">
 			<ul class="grafana-segment-list">
 			<ul class="grafana-segment-list">
-				<li class="grafana-target-segment">
-					<i class="icon-cogs"></i>
+				<li class="grafana-target-segment grafana-target-segment-icon">
+					<i class="icon-wrench"></i>
 				</li>
 				</li>
 				<li class="grafana-target-segment">
 				<li class="grafana-target-segment">
 					group by time
 					group by time
@@ -227,8 +227,8 @@
 <div class="editor-row">
 <div class="editor-row">
 	<div class="pull-left" style="margin-top: 30px;">
 	<div class="pull-left" style="margin-top: 30px;">
 
 
-		<div class="span6" ng-if="editorHelpIndex === 1">
-			Alias patterns:
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
+			<h5>Alias patterns</h5>
 			<ul>
 			<ul>
 				<li>$s = series name</li>
 				<li>$s = series name</li>
 				<li>$g = group by</li>
 				<li>$g = group by</li>
@@ -236,8 +236,8 @@
 			</ul>
 			</ul>
 		</div>
 		</div>
 
 
-		<div class="span6" ng-if="editorHelpIndex === 2">
-			Stacking and fill:
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
+			<h5>Stacking and fill</h5>
 			<ul>
 			<ul>
 				<li>When stacking is enabled it important that points align</li>
 				<li>When stacking is enabled it important that points align</li>
 				<li>If there are missing points for one series it can cause gaps or missing bars</li>
 				<li>If there are missing points for one series it can cause gaps or missing bars</li>
@@ -247,8 +247,8 @@
 			</ul>
 			</ul>
 		</div>
 		</div>
 
 
-		<div class="span6" ng-if="editorHelpIndex === 3">
-			Group by time:
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
+			<h5>Group by time</h5>
 			<ul>
 			<ul>
 				<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
 				<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
 				<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>
 				<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>

+ 1 - 1
src/app/partials/opentsdb/editor.html

@@ -30,7 +30,7 @@
           </li>
           </li>
         </ul>
         </ul>
 
 
-        <ul class="grafana-target-segment-list">
+        <ul class="grafana-segment-list">
           <li>
           <li>
             <a  class="grafana-target-segment"
             <a  class="grafana-target-segment"
                 ng-click="target.hide = !target.hide; get_data();"
                 ng-click="target.hide = !target.hide; get_data();"

+ 0 - 6
src/app/partials/paneladd.html

@@ -1,6 +0,0 @@
-<div ng-include="'app/partials/panelgeneral.html'"></div>
-<div ng-if="!panelMeta.fullEditorTabs" ng-include="edit_path(panel.type)"></div>
-<div ng-repeat="tab in panelMeta.editorTabs">
-  <h5>{{tab.title}}</h5>
-  <div ng-include="tab.src"></div>
-</div>

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

@@ -38,7 +38,7 @@
 				<div class="editor-option">
 				<div class="editor-option">
 					<div class="span4">
 					<div class="span4">
 						<span><i class="icon-question-sign"></i>
 						<span><i class="icon-question-sign"></i>
-							dashboards available in the playlist are only the once marked as favorites (stored in local browser storage).
+							dashboards available in the playlist are only the ones marked as favorites (stored in local browser storage).
 							to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite
 							to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite
 							<br/><br/>
 							<br/><br/>
 						</span>
 						</span>

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

@@ -54,7 +54,7 @@
 					<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
 					<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
 						<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
 						<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
 					</a>
 					</a>
-					<a ng-click="deleteDashboard(row.id, $event)">
+					<a ng-click="deleteDashboard(row, $event)">
 						<i class="icon-remove"></i> delete
 						<i class="icon-remove"></i> delete
 					</a>
 					</a>
 				</div>
 				</div>

+ 2 - 1
src/app/services/annotationsSrv.js

@@ -57,7 +57,8 @@ define([
 
 
     function errorHandler(err) {
     function errorHandler(err) {
       console.log('Annotation error: ', err);
       console.log('Annotation error: ', err);
-      alertSrv.set('Annotations','Could not fetch annotations','error');
+      var message = err.message || "Aannotation query failed";
+      alertSrv.set('Annotations error', message,'error');
     }
     }
 
 
     function addAnnotation(options) {
     function addAnnotation(options) {

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

@@ -18,8 +18,8 @@ function(angular, $) {
         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() {
       keyboardManager.bind('ctrl+f', function() {
         scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
         scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });

+ 1 - 1
src/app/services/dashboard/dashboardSrv.js

@@ -131,7 +131,7 @@ function (angular, $, kbn, _, moment) {
         if (old.services) {
         if (old.services) {
           if (old.services.filter) {
           if (old.services.filter) {
             this.time = old.services.filter.time;
             this.time = old.services.filter.time;
-            this.templating.list = old.services.filter.list;
+            this.templating.list = old.services.filter.list || [];
           }
           }
           delete this.services;
           delete this.services;
         }
         }

+ 32 - 23
src/app/services/dashboard/dashboardViewStateSrv.js

@@ -14,9 +14,12 @@ function (angular, _, $) {
     // like fullscreen panel & edit
     // like fullscreen panel & edit
     function DashboardViewState($scope) {
     function DashboardViewState($scope) {
       var self = this;
       var self = this;
+      self.state = {};
+      self.panelScopes = [];
+      self.$scope = $scope;
 
 
       $scope.exitFullscreen = function() {
       $scope.exitFullscreen = function() {
-        if (self.fullscreen) {
+        if (self.state.fullscreen) {
           self.update({ fullscreen: false });
           self.update({ fullscreen: false });
         }
         }
       };
       };
@@ -28,42 +31,48 @@ function (angular, _, $) {
         }
         }
       });
       });
 
 
-      this.panelScopes = [];
-      this.$scope = $scope;
-
       this.update(this.getQueryStringState(), true);
       this.update(this.getQueryStringState(), true);
     }
     }
 
 
     DashboardViewState.prototype.needsSync = function(urlState) {
     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;
+      return _.isEqual(this.state, urlState) === false;
     };
     };
 
 
     DashboardViewState.prototype.getQueryStringState = function() {
     DashboardViewState.prototype.getQueryStringState = function() {
       var queryParams = $location.search();
       var queryParams = $location.search();
-      return {
+      var urlState = {
         panelId: parseInt(queryParams.panelId) || null,
         panelId: parseInt(queryParams.panelId) || null,
         fullscreen: queryParams.fullscreen ? true : false,
         fullscreen: queryParams.fullscreen ? true : false,
-        edit: queryParams.edit ? true : false
+        edit: queryParams.edit ? true : false,
       };
       };
+
+      _.each(queryParams, function(value, key) {
+        if (key.indexOf('var-') !== 0) { return; }
+        urlState[key] = value;
+      });
+
+      return urlState;
+    };
+
+    DashboardViewState.prototype.serializeToUrl = function() {
+      var urlState = _.clone(this.state);
+      urlState.fullscreen = this.state.fullscreen ? true : null,
+      urlState.edit = this.state.edit ? true : null;
+
+      return urlState;
     };
     };
 
 
     DashboardViewState.prototype.update = function(state, skipUrlSync) {
     DashboardViewState.prototype.update = function(state, skipUrlSync) {
-      _.extend(this, state);
+      _.extend(this.state, state);
+      this.fullscreen = this.state.fullscreen;
 
 
-      if (!this.fullscreen) {
-        this.panelId = null;
-        this.edit = false;
+      if (!this.state.fullscreen) {
+        this.state.panelId = null;
+        this.state.edit = false;
       }
       }
 
 
       if (!skipUrlSync) {
       if (!skipUrlSync) {
-        $location.search({
-          fullscreen: this.fullscreen ? true : null,
-          panelId: this.panelId,
-          edit: this.edit ? true : null
-        });
+        $location.search(this.serializeToUrl());
       }
       }
 
 
       this.syncState();
       this.syncState();
@@ -76,7 +85,7 @@ function (angular, _, $) {
         if (this.fullscreenPanel) {
         if (this.fullscreenPanel) {
           this.leaveFullscreen(false);
           this.leaveFullscreen(false);
         }
         }
-        var panelScope = this.getPanelScope(this.panelId);
+        var panelScope = this.getPanelScope(this.state.panelId);
         this.enterFullscreen(panelScope);
         this.enterFullscreen(panelScope);
         return;
         return;
       }
       }
@@ -118,8 +127,8 @@ function (angular, _, $) {
       var fullscreenHeight = Math.floor(docHeight * 0.7);
       var fullscreenHeight = Math.floor(docHeight * 0.7);
       this.oldTimeRange = panelScope.range;
       this.oldTimeRange = panelScope.range;
 
 
-      panelScope.height = this.edit ? editHeight : fullscreenHeight;
-      panelScope.editMode = this.edit;
+      panelScope.height = this.state.edit ? editHeight : fullscreenHeight;
+      panelScope.editMode = this.state.edit;
       this.fullscreenPanel = panelScope;
       this.fullscreenPanel = panelScope;
 
 
       $(window).scrollTop(0);
       $(window).scrollTop(0);
@@ -135,7 +144,7 @@ function (angular, _, $) {
       var self = this;
       var self = this;
       self.panelScopes.push(panelScope);
       self.panelScopes.push(panelScope);
 
 
-      if (self.panelId === panelScope.panel.id) {
+      if (self.state.panelId === panelScope.panel.id) {
         self.enterFullscreen(panelScope);
         self.enterFullscreen(panelScope);
       }
       }
 
 

+ 1 - 1
src/app/services/datasourceSrv.js

@@ -80,7 +80,7 @@ function (angular, _, config) {
       if (!name) { return this.default; }
       if (!name) { return this.default; }
       if (datasources[name]) { return datasources[name]; }
       if (datasources[name]) { return datasources[name]; }
 
 
-      throw "Unable to find datasource: " + name;
+      return this.default;
     };
     };
 
 
     this.getAnnotationSources = function() {
     this.getAnnotationSources = function() {

+ 99 - 35
src/app/services/elasticsearch/es-datasource.js

@@ -1,12 +1,11 @@
 define([
 define([
   'angular',
   'angular',
   'lodash',
   'lodash',
-  'jquery',
   'config',
   'config',
   'kbn',
   'kbn',
   'moment'
   'moment'
 ],
 ],
-function (angular, _, $, config, kbn, moment) {
+function (angular, _, config, kbn, moment) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
@@ -38,6 +37,7 @@ function (angular, _, $, config, kbn, moment) {
       };
       };
 
 
       if (this.basicAuth) {
       if (this.basicAuth) {
+        options.withCredentials = true;
         options.headers = {
         options.headers = {
           "Authorization": "Basic " + this.basicAuth
           "Authorization": "Basic " + this.basicAuth
         };
         };
@@ -76,57 +76,78 @@ function (angular, _, $, config, kbn, moment) {
       var queryInterpolated = templateSrv.replace(queryString);
       var queryInterpolated = templateSrv.replace(queryString);
       var filter = { "bool": { "must": [{ "range": range }] } };
       var filter = { "bool": { "must": [{ "range": range }] } };
       var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
       var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
-      var data = { "query" : { "filtered": { "query" : query, "filter": filter } }, "size": 100 };
+      var data = {
+        "fields": [timeField, "_source"],
+        "query" : { "filtered": { "query" : query, "filter": filter } },
+        "size": 100
+      };
 
 
       return this._request('POST', '/_search', annotation.index, data).then(function(results) {
       return this._request('POST', '/_search', annotation.index, data).then(function(results) {
         var list = [];
         var list = [];
         var hits = results.data.hits.hits;
         var hits = results.data.hits.hits;
 
 
+        var getFieldFromSource = function(source, fieldName) {
+          if (!fieldName) { return; }
+
+          var fieldNames = fieldName.split('.');
+          var fieldValue = source;
+
+          for (var i = 0; i < fieldNames.length; i++) {
+            fieldValue = fieldValue[fieldNames[i]];
+          }
+
+          if (_.isArray(fieldValue)) {
+            fieldValue = fieldValue.join(', ');
+          }
+          return fieldValue;
+        };
+
         for (var i = 0; i < hits.length; i++) {
         for (var i = 0; i < hits.length; i++) {
           var source = hits[i]._source;
           var source = hits[i]._source;
+          var fields = hits[i].fields;
+          var time = source[timeField];
+
+          if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) {
+            time = fields[timeField];
+          }
+
           var event = {
           var event = {
             annotation: annotation,
             annotation: annotation,
-            time: moment.utc(source[timeField]).valueOf(),
-            title: source[titleField],
+            time: moment.utc(time).valueOf(),
+            title: getFieldFromSource(source, titleField),
+            tags: getFieldFromSource(source, tagsField),
+            text: getFieldFromSource(source, textField)
           };
           };
 
 
-          if (source[tagsField]) {
-            if (_.isArray(source[tagsField])) {
-              event.tags = source[tagsField].join(', ');
-            }
-            else {
-              event.tags = source[tagsField];
-            }
-          }
-          if (textField && source[textField]) {
-            event.text = source[textField];
-          }
-
           list.push(event);
           list.push(event);
         }
         }
         return list;
         return list;
       });
       });
     };
     };
 
 
+    ElasticDatasource.prototype._getDashboardWithSlug = function(id) {
+      return this._get('/dashboard/' + kbn.slugifyForUrl(id))
+        .then(function(result) {
+          return angular.fromJson(result._source.dashboard);
+        }, function() {
+          throw "Dashboard not found";
+        });
+    };
+
     ElasticDatasource.prototype.getDashboard = function(id, isTemp) {
     ElasticDatasource.prototype.getDashboard = function(id, isTemp) {
       var url = '/dashboard/' + id;
       var url = '/dashboard/' + id;
+      if (isTemp) { url = '/temp/' + id; }
 
 
-      if (isTemp) {
-        url = '/temp/' + id;
-      }
-
+      var self = this;
       return this._get(url)
       return this._get(url)
         .then(function(result) {
         .then(function(result) {
-          if (result._source && result._source.dashboard) {
-            return angular.fromJson(result._source.dashboard);
-          } else {
-            return false;
-          }
+          return angular.fromJson(result._source.dashboard);
         }, function(data) {
         }, function(data) {
           if(data.status === 0) {
           if(data.status === 0) {
             throw "Could not contact Elasticsearch. Please ensure that Elasticsearch is reachable from your browser.";
             throw "Could not contact Elasticsearch. Please ensure that Elasticsearch is reachable from your browser.";
           } else {
           } else {
-            throw "Could not find dashboard " + id;
+            // backward compatible fallback
+            return self._getDashboardWithSlug(id);
           }
           }
         });
         });
     };
     };
@@ -148,15 +169,30 @@ function (angular, _, $, config, kbn, moment) {
         return this._saveTempDashboard(data);
         return this._saveTempDashboard(data);
       }
       }
       else {
       else {
-        return this._request('PUT', '/dashboard/' + encodeURIComponent(title), this.index, data)
-          .then(function() {
-            return { title: title, url: '/dashboard/db/' + title };
-          }, function(err) {
-            throw 'Failed to save to elasticsearch ' + err.data;
+
+        var id = encodeURIComponent(kbn.slugifyForUrl(title));
+        var self = this;
+
+        return this._request('PUT', '/dashboard/' + id, this.index, data)
+          .then(function(results) {
+            self._removeUnslugifiedDashboard(results, title, id);
+            return { title: title, url: '/dashboard/db/' + id };
+          }, function() {
+            throw 'Failed to save to elasticsearch';
           });
           });
       }
       }
     };
     };
 
 
+    ElasticDatasource.prototype._removeUnslugifiedDashboard = function(saveResult, title, id) {
+      if (saveResult.statusText !== 'Created') { return; }
+      if (title === id) { return; }
+
+      var self = this;
+      this._get('/dashboard/' + title).then(function() {
+        self.deleteDashboard(title);
+      });
+    };
+
     ElasticDatasource.prototype._saveTempDashboard = function(data) {
     ElasticDatasource.prototype._saveTempDashboard = function(data) {
       return this._request('POST', '/temp/?ttl=' + this.saveTempTTL, this.index, data)
       return this._request('POST', '/temp/?ttl=' + this.saveTempTTL, this.index, data)
         .then(function(result) {
         .then(function(result) {
@@ -181,7 +217,21 @@ function (angular, _, $, config, kbn, moment) {
     };
     };
 
 
     ElasticDatasource.prototype.searchDashboards = function(queryString) {
     ElasticDatasource.prototype.searchDashboards = function(queryString) {
-      queryString = queryString.toLowerCase().replace(' and ', ' AND ');
+      var endsInOpen = function(string, opener, closer) {
+        var character;
+        var count = 0;
+        for (var i=0; i<string.length; i++) {
+          character = string[i];
+
+          if (character === opener) {
+            count++;
+          } else if (character === closer) {
+            count--;
+          }
+        }
+
+        return count > 0;
+      };
 
 
       var tagsOnly = queryString.indexOf('tags!:') === 0;
       var tagsOnly = queryString.indexOf('tags!:') === 0;
       if (tagsOnly) {
       if (tagsOnly) {
@@ -193,7 +243,21 @@ function (angular, _, $, config, kbn, moment) {
           queryString = 'title:';
           queryString = 'title:';
         }
         }
 
 
-        if (queryString[queryString.length - 1] !== '*') {
+        // make this a partial search if we're not in some reserved portion of the language,  comments on conditionals, in order:
+        // 1. ends in reserved character, boosting, boolean operator ( -foo)
+        // 2. typing a reserved word like AND, OR, NOT
+        // 3. open parens (groupiing)
+        // 4. open " (term phrase)
+        // 5. open [ (range)
+        // 6. open { (range)
+        // see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
+        if (!queryString.match(/(\*|\]|}|~|\)|"|^\d+|\s[\-+]\w+)$/) &&
+            !queryString.match(/[A-Z]$/) &&
+            !endsInOpen(queryString, '(', ')') &&
+            !endsInOpen(queryString, '"', '"') &&
+            !endsInOpen(queryString, '[', ']') && !endsInOpen(queryString, '[', '}') &&
+            !endsInOpen(queryString, '{', ']') && !endsInOpen(queryString, '{', '}')
+        ){
           queryString += '*';
           queryString += '*';
         }
         }
       }
       }
@@ -216,7 +280,7 @@ function (angular, _, $, config, kbn, moment) {
           for (var i = 0; i < results.hits.hits.length; i++) {
           for (var i = 0; i < results.hits.hits.length; i++) {
             hits.dashboards.push({
             hits.dashboards.push({
               id: results.hits.hits[i]._id,
               id: results.hits.hits[i]._id,
-              title: results.hits.hits[i]._id,
+              title: results.hits.hits[i]._source.title,
               tags: results.hits.hits[i]._source.tags
               tags: results.hits.hits[i]._source.tags
             });
             });
           }
           }

+ 15 - 1
src/app/services/graphite/gfunc.js

@@ -68,7 +68,14 @@ function (_) {
   addFuncDef({
   addFuncDef({
     name: 'diffSeries',
     name: 'diffSeries',
     params: optionalSeriesRefArgs,
     params: optionalSeriesRefArgs,
-    defaultParams: ['#B'],
+    defaultParams: ['#A'],
+    category: categories.Calculate,
+  });
+
+  addFuncDef({
+    name: 'divideSeries',
+    params: optionalSeriesRefArgs,
+    defaultParams: ['#A'],
     category: categories.Calculate,
     category: categories.Calculate,
   });
   });
 
 
@@ -79,6 +86,13 @@ function (_) {
     category: categories.Calculate,
     category: categories.Calculate,
   });
   });
 
 
+  addFuncDef({
+    name: 'group',
+    params: optionalSeriesRefArgs,
+    defaultParams: ['#A', '#B'],
+    category: categories.Combine,
+  });
+
   addFuncDef({
   addFuncDef({
     name: 'sumSeries',
     name: 'sumSeries',
     shortName: 'sum',
     shortName: 'sum',

+ 60 - 17
src/app/services/influxdb/influxdbDatasource.js

@@ -19,6 +19,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       this.username = datasource.username;
       this.username = datasource.username;
       this.password = datasource.password;
       this.password = datasource.password;
       this.name = datasource.name;
       this.name = datasource.name;
+      this.basicAuth = datasource.basicAuth;
 
 
       this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
       this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
       this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
       this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
@@ -63,7 +64,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
     InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
     InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
       var timeFilter = getTimeFilter({ range: rangeUnparsed });
       var timeFilter = getTimeFilter({ range: rangeUnparsed });
       var query = annotation.query.replace('$timeFilter', timeFilter);
       var query = annotation.query.replace('$timeFilter', timeFilter);
-      query = templateSrv.replace(annotation.query);
+      query = templateSrv.replace(query);
 
 
       return this._seriesQuery(query).then(function(results) {
       return this._seriesQuery(query).then(function(results) {
         return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
         return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
@@ -170,6 +171,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
           inspect: { type: 'influxdb' },
           inspect: { type: 'influxdb' },
         };
         };
 
 
+        options.headers = options.headers || {};
+        if (_this.basicAuth) {
+          options.headers.Authorization = 'Basic ' + _this.basicAuth;
+        }
+
         return $http(options).success(function (data) {
         return $http(options).success(function (data) {
           deferred.resolve(data);
           deferred.resolve(data);
         });
         });
@@ -182,34 +188,46 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       var tags = dashboard.tags.join(',');
       var tags = dashboard.tags.join(',');
       var title = dashboard.title;
       var title = dashboard.title;
       var temp = dashboard.temp;
       var temp = dashboard.temp;
+      var id = kbn.slugifyForUrl(title);
       if (temp) { delete dashboard.temp; }
       if (temp) { delete dashboard.temp; }
 
 
       var data = [{
       var data = [{
-        name: 'grafana.dashboard_' + btoa(title),
-        columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard'],
-        points: [[1000000000000, 1, title, tags, angular.toJson(dashboard)]]
+        name: 'grafana.dashboard_' + btoa(id),
+        columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'],
+        points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]]
       }];
       }];
 
 
       if (temp) {
       if (temp) {
-        return this._saveDashboardTemp(data, title);
+        return this._saveDashboardTemp(data, title, id);
       }
       }
       else {
       else {
+        var self = this;
         return this._influxRequest('POST', '/series', data).then(function() {
         return this._influxRequest('POST', '/series', data).then(function() {
-          return { title: title, url: '/dashboard/db/' + title };
+          self._removeUnslugifiedDashboard(title, false);
+          return { title: title, url: '/dashboard/db/' + id };
         }, function(err) {
         }, function(err) {
           throw 'Failed to save dashboard to InfluxDB: ' + err.data;
           throw 'Failed to save dashboard to InfluxDB: ' + err.data;
         });
         });
       }
       }
     };
     };
 
 
-    InfluxDatasource.prototype._saveDashboardTemp = function(data, title) {
-      data[0].name = 'grafana.temp_dashboard_' + btoa(title);
+    InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, isTemp) {
+      var self = this;
+      self._getDashboardInternal(id, isTemp).then(function(dashboard) {
+        if (dashboard !== null) {
+          self.deleteDashboard(id);
+        }
+      });
+    };
+
+    InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) {
+      data[0].name = 'grafana.temp_dashboard_' + btoa(id);
       data[0].columns.push('expires');
       data[0].columns.push('expires');
       data[0].points[0].push(this._getTempDashboardExpiresDate());
       data[0].points[0].push(this._getTempDashboardExpiresDate());
 
 
       return this._influxRequest('POST', '/series', data).then(function() {
       return this._influxRequest('POST', '/series', data).then(function() {
         var baseUrl = window.location.href.replace(window.location.hash,'');
         var baseUrl = window.location.href.replace(window.location.hash,'');
-        var url = baseUrl + "#dashboard/temp/" + title;
+        var url = baseUrl + "#dashboard/temp/" + id;
         return { title: title, url: url };
         return { title: title, url: url };
       }, function(err) {
       }, function(err) {
         throw 'Failed to save shared dashboard to InfluxDB: ' + err.data;
         throw 'Failed to save shared dashboard to InfluxDB: ' + err.data;
@@ -236,7 +254,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       return expires;
       return expires;
     };
     };
 
 
-    InfluxDatasource.prototype.getDashboard = function(id, isTemp) {
+    InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) {
       var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"';
       var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"';
 
 
       if (isTemp) {
       if (isTemp) {
@@ -245,15 +263,34 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
 
 
       return this._seriesQuery(queryString).then(function(results) {
       return this._seriesQuery(queryString).then(function(results) {
         if (!results || !results.length) {
         if (!results || !results.length) {
-          throw "Dashboard not found";
+          return null;
         }
         }
 
 
         var dashCol = _.indexOf(results[0].columns, 'dashboard');
         var dashCol = _.indexOf(results[0].columns, 'dashboard');
         var dashJson = results[0].points[0][dashCol];
         var dashJson = results[0].points[0][dashCol];
 
 
         return angular.fromJson(dashJson);
         return angular.fromJson(dashJson);
+      }, function() {
+        return null;
+      });
+    };
+
+    InfluxDatasource.prototype.getDashboard = function(id, isTemp) {
+      var self = this;
+      return this._getDashboardInternal(id, isTemp).then(function(dashboard) {
+        if (dashboard !== null)  {
+          return dashboard;
+        }
+
+        // backward compatible load for unslugified ids
+        var slug = kbn.slugifyForUrl(id);
+        if (slug !== id) {
+          return self.getDashboard(slug, isTemp);
+        }
+
+        throw "Dashboard not found";
       }, function(err) {
       }, function(err) {
-        return "Could not load dashboard, " + err.data;
+        throw  "Could not load dashboard, " + err.data;
       });
       });
     };
     };
 
 
@@ -264,12 +301,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
         }
         }
         return id;
         return id;
       }, function(err) {
       }, function(err) {
-        return "Could not delete dashboard, " + err.data;
+        throw "Could not delete dashboard, " + err.data;
       });
       });
     };
     };
 
 
     InfluxDatasource.prototype.searchDashboards = function(queryString) {
     InfluxDatasource.prototype.searchDashboards = function(queryString) {
-      var influxQuery = 'select title, tags from /grafana.dashboard_.*/ where ';
+      var influxQuery = 'select * from /grafana.dashboard_.*/ where ';
 
 
       var tagsOnly = queryString.indexOf('tags!:') === 0;
       var tagsOnly = queryString.indexOf('tags!:') === 0;
       if (tagsOnly) {
       if (tagsOnly) {
@@ -294,15 +331,21 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
           return hits;
           return hits;
         }
         }
 
 
-        var dashCol = _.indexOf(results[0].columns, 'title');
-        var tagsCol = _.indexOf(results[0].columns, 'tags');
-
         for (var i = 0; i < results.length; i++) {
         for (var i = 0; i < results.length; i++) {
+          var dashCol = _.indexOf(results[i].columns, 'title');
+          var tagsCol = _.indexOf(results[i].columns, 'tags');
+          var idCol = _.indexOf(results[i].columns, 'id');
+
           var hit =  {
           var hit =  {
             id: results[i].points[0][dashCol],
             id: results[i].points[0][dashCol],
             title: results[i].points[0][dashCol],
             title: results[i].points[0][dashCol],
             tags: results[i].points[0][tagsCol].split(",")
             tags: results[i].points[0][tagsCol].split(",")
           };
           };
+
+          if (idCol !== -1) {
+            hit.id = results[i].points[0][idCol];
+          }
+
           hit.tags = hit.tags[0] ? hit.tags : [];
           hit.tags = hit.tags[0] ? hit.tags : [];
           hits.dashboards.push(hit);
           hits.dashboards.push(hit);
         }
         }

+ 1 - 1
src/app/services/opentsdb/opentsdbDatasource.js

@@ -19,7 +19,7 @@ function (angular, _, kbn) {
     }
     }
 
 
     // Called once per panel (graph)
     // Called once per panel (graph)
-    OpenTSDBDatasource.prototype.query = function(filterSrv, options) {
+    OpenTSDBDatasource.prototype.query = function(options) {
       var start = convertToTSDBTime(options.range.from);
       var start = convertToTSDBTime(options.range.from);
       var end = convertToTSDBTime(options.range.to);
       var end = convertToTSDBTime(options.range.to);
       var queries = _.compact(_.map(options.targets, convertTargetToQuery));
       var queries = _.compact(_.map(options.targets, convertTargetToQuery));

+ 34 - 22
src/app/services/templateSrv.js

@@ -7,37 +7,29 @@ function (angular, _) {
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('templateSrv', function($q, $routeParams) {
+  module.service('templateSrv', function() {
     var self = this;
     var self = this;
 
 
     this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
     this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
-    this._templateData = {};
+    this._values = {};
+    this._texts = {};
     this._grafanaVariables = {};
     this._grafanaVariables = {};
 
 
     this.init = function(variables) {
     this.init = function(variables) {
       this.variables = variables;
       this.variables = variables;
-      this.updateTemplateData(true);
+      this.updateTemplateData();
     };
     };
 
 
-    this.updateTemplateData = function(initial) {
-      var data = {};
+    this.updateTemplateData = function() {
+      this._values = {};
+      this._texts = {};
 
 
       _.each(this.variables, function(variable) {
       _.each(this.variables, function(variable) {
-        if (initial) {
-          var urlValue = $routeParams[ variable.name ];
-          if (urlValue) {
-            variable.current = { text: urlValue, value: urlValue };
-          }
-        }
-
-        if (!variable.current || !variable.current.value) {
-          return;
-        }
-
-        data[variable.name] = variable.current.value;
-      });
+        if (!variable.current || !variable.current.value) { return; }
 
 
-      this._templateData = data;
+        this._values[variable.name] = variable.current.value;
+        this._texts[variable.name] = variable.current.text;
+      }, this);
     };
     };
 
 
     this.setGrafanaVariable = function (name, value) {
     this.setGrafanaVariable = function (name, value) {
@@ -47,7 +39,11 @@ function (angular, _) {
     this.variableExists = function(expression) {
     this.variableExists = function(expression) {
       this._regex.lastIndex = 0;
       this._regex.lastIndex = 0;
       var match = this._regex.exec(expression);
       var match = this._regex.exec(expression);
-      return match && (self._templateData[match[1] || match[2]] !== void 0);
+      return match && (self._values[match[1] || match[2]] !== void 0);
+    };
+
+    this.containsVariable = function(str, variableName) {
+      return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1;
     };
     };
 
 
     this.highlightVariablesAsHtml = function(str) {
     this.highlightVariablesAsHtml = function(str) {
@@ -55,7 +51,7 @@ function (angular, _) {
 
 
       this._regex.lastIndex = 0;
       this._regex.lastIndex = 0;
       return str.replace(this._regex, function(match, g1, g2) {
       return str.replace(this._regex, function(match, g1, g2) {
-        if (self._templateData[g1 || g2]) {
+        if (self._values[g1 || g2]) {
           return '<span class="template-variable">' + match + '</span>';
           return '<span class="template-variable">' + match + '</span>';
         }
         }
         return match;
         return match;
@@ -69,13 +65,29 @@ function (angular, _) {
       this._regex.lastIndex = 0;
       this._regex.lastIndex = 0;
 
 
       return target.replace(this._regex, function(match, g1, g2) {
       return target.replace(this._regex, function(match, g1, g2) {
-        value = self._templateData[g1 || g2];
+        value = self._values[g1 || g2];
         if (!value) { return match; }
         if (!value) { return match; }
 
 
         return self._grafanaVariables[value] || value;
         return self._grafanaVariables[value] || value;
       });
       });
     };
     };
 
 
+    this.replaceWithText = function(target) {
+      if (!target) { return; }
+
+      var value;
+      var text;
+      this._regex.lastIndex = 0;
+
+      return target.replace(this._regex, function(match, g1, g2) {
+        value = self._values[g1 || g2];
+        text = self._texts[g1 || g2];
+        if (!value) { return match; }
+
+        return self._grafanaVariables[value] || text;
+      });
+    };
+
   });
   });
 
 
 });
 });

+ 17 - 11
src/app/services/templateValuesSrv.js

@@ -18,17 +18,24 @@ function (angular, _, kbn) {
       }
       }
     });
     });
 
 
-    this.init = function(dashboard) {
+    this.init = function(dashboard, viewstate) {
       this.variables = dashboard.templating.list;
       this.variables = dashboard.templating.list;
+      this.viewstate = viewstate;
       templateSrv.init(this.variables);
       templateSrv.init(this.variables);
 
 
       for (var i = 0; i < this.variables.length; i++) {
       for (var i = 0; i < this.variables.length; i++) {
-        var param = this.variables[i];
-        if (param.refresh) {
-          this.updateOptions(param);
+        var variable = this.variables[i];
+        var urlValue = viewstate.state['var-' + variable.name];
+        if (urlValue !== void 0) {
+          var option = _.findWhere(variable.options, { text: urlValue });
+          option = option || { text: urlValue, value: urlValue };
+          this.setVariableValue(variable, option, true);
         }
         }
-        else if (param.type === 'interval') {
-          this.updateAutoInterval(param);
+        else if (variable.refresh) {
+          this.updateOptions(variable);
+        }
+        else if (variable.type === 'interval') {
+          this.updateAutoInterval(variable);
         }
         }
       }
       }
     };
     };
@@ -63,7 +70,7 @@ function (angular, _, kbn) {
         if (otherVariable === updatedVariable) {
         if (otherVariable === updatedVariable) {
           return;
           return;
         }
         }
-        if (otherVariable.query.indexOf('[[' + updatedVariable.name + ']]') !== -1) {
+        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
           return self.updateOptions(otherVariable);
           return self.updateOptions(otherVariable);
         }
         }
       });
       });
@@ -92,7 +99,6 @@ function (angular, _, kbn) {
       var datasource = datasourceSrv.get(variable.datasource);
       var datasource = datasourceSrv.get(variable.datasource);
       return datasource.metricFindQuery(variable.query)
       return datasource.metricFindQuery(variable.query)
         .then(function (results) {
         .then(function (results) {
-
           variable.options = self.metricNamesToVariableValues(variable, results);
           variable.options = self.metricNamesToVariableValues(variable, results);
 
 
           if (variable.includeAll) {
           if (variable.includeAll) {
@@ -102,9 +108,9 @@ function (angular, _, kbn) {
           // if parameter has current value
           // if parameter has current value
           // if it exists in options array keep value
           // if it exists in options array keep value
           if (variable.current) {
           if (variable.current) {
-            var currentExists = _.findWhere(variable.options, { value: variable.current.value });
-            if (currentExists) {
-              return self.setVariableValue(variable, variable.current, true);
+            var currentOption = _.findWhere(variable.options, { text: variable.current.text });
+            if (currentOption) {
+              return self.setVariableValue(variable, currentOption, true);
             }
             }
           }
           }
 
 

+ 33 - 3
src/app/services/timeSrv.js

@@ -2,13 +2,14 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'config',
   'config',
-  'kbn'
-], function (angular, _, config, kbn) {
+  'kbn',
+  'moment'
+], function (angular, _, config, kbn, moment) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('timeSrv', function($rootScope, $timeout, timer) {
+  module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
     var self = this;
     var self = this;
 
 
     this.init = function(dashboard) {
     this.init = function(dashboard) {
@@ -17,11 +18,40 @@ define([
       this.dashboard = dashboard;
       this.dashboard = dashboard;
       this.time = dashboard.time;
       this.time = dashboard.time;
 
 
+      this._initTimeFromUrl();
+
       if(this.dashboard.refresh) {
       if(this.dashboard.refresh) {
         this.set_interval(this.dashboard.refresh);
         this.set_interval(this.dashboard.refresh);
       }
       }
     };
     };
 
 
+    this._parseUrlParam = function(value) {
+      if (value.indexOf('now') !== -1) {
+        return value;
+      }
+      if (value.length === 8) {
+        return moment.utc(value, 'YYYYMMDD').toDate();
+      }
+      if (value.length === 15) {
+        return moment.utc(value, 'YYYYMMDDTHHmmss').toDate();
+      }
+      var epoch = parseInt(value);
+      if (!_.isNaN(epoch)) {
+        return new Date(epoch);
+      }
+
+      return null;
+    };
+
+    this._initTimeFromUrl = function() {
+      if ($routeParams.from) {
+        this.time.from = this._parseUrlParam($routeParams.from) || this.time.from;
+      }
+      if ($routeParams.to) {
+        this.time.to = this._parseUrlParam($routeParams.to) || this.time.to;
+      }
+    };
+
     this.set_interval = function (interval) {
     this.set_interval = function (interval) {
       this.dashboard.refresh = interval;
       this.dashboard.refresh = interval;
       if (interval) {
       if (interval) {

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

@@ -83,6 +83,14 @@ function(angular, _, config) {
       current.time = original.time = {};
       current.time = original.time = {};
       current.refresh = original.refresh;
       current.refresh = original.refresh;
 
 
+      // ignore template variable values
+      _.each(current.templating.list, function(value, index) {
+        value.current = null;
+        value.options = null;
+        original.templating.list[index].current = null;
+        original.templating.list[index].options = null;
+      });
+
       var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' });
       var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' });
       var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' });
       var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' });
 
 

+ 5 - 6
src/config.sample.js

@@ -1,8 +1,7 @@
-///// @scratch /configuration/config.js/1
- // == Configuration
- // config.js is where you will find the core Grafana configuration. This file contains parameter that
- // must be set before Grafana is run for the first time.
- ///
+// == Configuration
+// config.js is where you will find the core Grafana configuration. This file contains parameter that
+// must be set before Grafana is run for the first time.
+
 define(['settings'],
 define(['settings'],
 function (Settings) {
 function (Settings) {
   "use strict";
   "use strict";
@@ -97,7 +96,7 @@ function (Settings) {
     // Change window title prefix from 'Grafana - <dashboard title>'
     // Change window title prefix from 'Grafana - <dashboard title>'
     window_title_prefix: 'Grafana - ',
     window_title_prefix: 'Grafana - ',
 
 
-    // Add your own custom pannels
+    // Add your own custom panels
     plugins: {
     plugins: {
       // list of plugin panels
       // list of plugin panels
       panels: [],
       panels: [],

+ 1 - 1
src/css/less/bootswatch.dark.less

@@ -56,7 +56,7 @@ hr {
 	}
 	}
 
 
 	.brand {
 	.brand {
-		padding: 15px 20px 15px;
+		padding: 0px 15px;
 		color: @grayLighter;
 		color: @grayLighter;
 		font-weight: normal;
 		font-weight: normal;
 		text-shadow: none;
 		text-shadow: none;

+ 1 - 1
src/css/less/bootswatch.light.less

@@ -59,6 +59,7 @@ a.text-success:hover { color: darken(@green, 10%); }
 	}
 	}
 
 
 	.brand {
 	.brand {
+		padding: 0px 15px;
 
 
 		&:hover {
 		&:hover {
 			color: @navbarLinkColorHover;
 			color: @navbarLinkColorHover;
@@ -461,7 +462,6 @@ legend {
 // -----------------------------------------------------
 // -----------------------------------------------------
 
 
 .alert {
 .alert {
-	.border-radius(0);
 	text-shadow: none;
 	text-shadow: none;
 
 
 	&-heading, h1, h2, h3, h4, h5, h6 {
 	&-heading, h1, h2, h3, h4, h5, h6 {

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

@@ -35,6 +35,19 @@
   }
   }
 }
 }
 
 
+.logo-icon {
+  width: 24px;
+  padding: 13px 11px 0 0;
+  display: block;
+  float: left;
+}
+
+.page-title {
+ padding: 15px 0;
+ display: block;
+ float: left;
+}
+
 .row-button {
 .row-button {
   width: 24px;
   width: 24px;
 }
 }
@@ -87,7 +100,7 @@
 
 
 .panel-fullscreen {
 .panel-fullscreen {
   z-index: 100;
   z-index: 100;
-  display: block !important;
+  display: block;
   position: fixed;
   position: fixed;
   left: 0px;
   left: 0px;
   right: 0px;
   right: 0px;
@@ -103,11 +116,10 @@
   }
   }
 }
 }
 
 
-.dashboard-fullscreen .main-view-container {
-  height: 0px;
-  width: 0px;
-  position: fixed;
-  right: -10000px;
+.dashboard-fullscreen {
+  .row-control-inner {
+    display: none;
+  }
 }
 }
 
 
 .histogram-chart {
 .histogram-chart {
@@ -423,6 +435,9 @@ select.grafana-target-segment-input {
   background-color: rgb(58, 57, 57);
   background-color: rgb(58, 57, 57);
   border-radius: 5px;
   border-radius: 5px;
   z-index: 9999;
   z-index: 9999;
+  max-width: 800px;
+  max-height: 600px;
+  overflow: hidden;
 }
 }
 
 
 .tooltip.in {
 .tooltip.in {
@@ -485,3 +500,26 @@ select.grafana-target-segment-input {
   color: @variable;
   color: @variable;
 }
 }
 
 
+.grafana-info-box:before {
+  content: "\f05a";
+  font-family:'FontAwesome';
+  position: absolute;
+  top: -8px;
+  left: -8px;
+  font-size: 20px;
+  color: @blue;
+}
+
+.grafana-info-box {
+  position: relative;
+  padding: 5px 15px;
+  background-color: @grafanaTargetBackground;
+  border: 1px solid @grafanaTargetBorder;
+  h5 {
+    margin-top: 5px;
+  }
+}
+
+.grafana-tip {
+  padding-left: 5px;
+}

+ 36 - 6
src/css/less/overrides.less

@@ -317,13 +317,38 @@ div.flot-text {
   color: @textColor !important;
   color: @textColor !important;
 }
 }
 
 
-.dashboard-notice {
+.page-alert-list {
   z-index:8000;
   z-index:8000;
-  margin-left:0px;
-  padding:3px 0px 3px 0px;
-  width:100%;
-  padding-left:20px;
-  color: @white;
+  min-width: 300px;
+  max-width: 300px;
+  position: fixed;
+  right: 20px;
+  top: 56px;
+
+  .alert {
+    color: @white;
+    padding-bottom: 13px;
+    position: relative;
+  }
+
+  .alert-close {
+    position: absolute;
+    top: -4px;
+    right: -2px;
+    width: 19px;
+    height: 19px;
+    padding: 0;
+    background: @grayLighter;
+    border-radius: 50%;
+    border: none;
+    font-size: 1.1rem;
+    color: @grayDarker;
+  }
+
+  .alert-title {
+    font-weight: bold;
+    padding-bottom: 2px;
+  }
 }
 }
 
 
 
 
@@ -516,6 +541,11 @@ div.flot-text {
   }
   }
 }
 }
 
 
+// typeahead max height
+.typeahead {
+  max-height: 300px;
+  overflow-y: auto;
+}
 
 
 // Labels & Badges
 // Labels & Badges
 .label-tag {
 .label-tag {

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

@@ -257,7 +257,7 @@
 // Form states and alerts
 // Form states and alerts
 // -------------------------
 // -------------------------
 @warningText:             darken(#c09853, 10%);
 @warningText:             darken(#c09853, 10%);
-@warningBackground:       @grayLighter;
+@warningBackground:       @orange;
 @warningBorder:           transparent;
 @warningBorder:           transparent;
 
 
 @errorText:               #b94a48;
 @errorText:               #b94a48;

BIN
src/img/fav16.png


BIN
src/img/fav32.png


BIN
src/img/fav_dark_16.png


BIN
src/img/fav_dark_32.png


+ 13 - 6
src/index.html

@@ -5,9 +5,11 @@
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta name="viewport" content="width=device-width">
     <meta name="viewport" content="width=device-width">
+    <meta name="google" value="notranslate">
 
 
     <title>Grafana</title>
     <title>Grafana</title>
     <link rel="stylesheet" href="css/grafana.dark.min.css" title="Dark">
     <link rel="stylesheet" href="css/grafana.dark.min.css" title="Dark">
+    <link rel="icon" type="image/png" href="img/fav32.png">
 
 
     <!-- build:js app/app.js -->
     <!-- build:js app/app.js -->
     <script src="vendor/require/require.js"></script>
     <script src="vendor/require/require.js"></script>
@@ -22,12 +24,17 @@
 
 
     <link rel="stylesheet" href="css/grafana.light.min.css" ng-if="grafana.style === 'light'">
     <link rel="stylesheet" href="css/grafana.light.min.css" ng-if="grafana.style === 'light'">
 
 
-    <div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last">
-      <button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button>
-      <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 class="page-alert-list">
+			<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
+				<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
+					<i class="icon-remove-sign"></i>
+				</button>
+				<div class="alert-title">{{alert.title}}</div>
+				<div ng-bind-html='alert.text'></div>
+			</div>
+		</div>
 
 
-    <div ng-view></div>
+		<div ng-view></div>
 
 
-  </body>
+	</body>
 </html>
 </html>

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

@@ -20,6 +20,7 @@ define([
         viewState.update(updateState);
         viewState.update(updateState);
         expect(location.search()).to.eql(updateState);
         expect(location.search()).to.eql(updateState);
         expect(viewState.fullscreen).to.be(true);
         expect(viewState.fullscreen).to.be(true);
+        expect(viewState.state.fullscreen).to.be(true);
       });
       });
     });
     });
 
 
@@ -29,6 +30,7 @@ define([
         viewState.update({fullscreen: false});
         viewState.update({fullscreen: false});
         expect(location.search()).to.eql({});
         expect(location.search()).to.eql({});
         expect(viewState.fullscreen).to.be(false);
         expect(viewState.fullscreen).to.be(false);
+        expect(viewState.state.fullscreen).to.be(false);
       });
       });
     });
     });
 
 

+ 19 - 0
src/test/specs/graphiteTargetCtrl-specs.js

@@ -99,6 +99,25 @@ define([
 
 
     });
     });
 
 
+    describe('when initializing a target with single param func using variable', function() {
+      beforeEach(function() {
+        ctx.scope.target.target = 'movingAverage(prod.count, $var)';
+        ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
+        ctx.scope.init();
+        ctx.scope.$digest();
+        ctx.scope.$parent = { get_data: sinon.spy() };
+      });
+
+      it('should add 2 segments', function() {
+        expect(ctx.scope.segments.length).to.be(2);
+      });
+
+      it('should add function param', function() {
+        expect(ctx.scope.functions[0].params.length).to.be(1);
+      });
+
+    });
+
     describe('when initalizing target without metric expression and function with series-ref', function() {
     describe('when initalizing target without metric expression and function with series-ref', function() {
       beforeEach(function() {
       beforeEach(function() {
         ctx.scope.target.target = 'asPercent(metric.node.count, #A)';
         ctx.scope.target.target = 'asPercent(metric.node.count, #A)';

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

@@ -51,6 +51,7 @@ define([
     self.templateSrv = new TemplateSrvStub();
     self.templateSrv = new TemplateSrvStub();
     self.timeSrv = new TimeSrvStub();
     self.timeSrv = new TimeSrvStub();
     self.datasourceSrv = {};
     self.datasourceSrv = {};
+    self.$routeParams = {};
 
 
     this.providePhase = function(mocks) {
     this.providePhase = function(mocks) {
      return module(function($provide) {
      return module(function($provide) {
@@ -103,6 +104,7 @@ define([
     this.replace = function(text) {
     this.replace = function(text) {
       return _.template(text, this.data,  this.templateSettings);
       return _.template(text, this.data,  this.templateSettings);
     };
     };
+    this.init = function() {};
     this.updateTemplateData = function() { };
     this.updateTemplateData = function() { };
     this.variableExists = function() { return false; };
     this.variableExists = function() { return false; };
     this.highlightVariablesAsHtml = function(str) { return str; };
     this.highlightVariablesAsHtml = function(str) { return str; };

+ 25 - 1
src/test/specs/influxdb-datasource-specs.js

@@ -8,7 +8,7 @@ define([
     var ctx = new helpers.ServiceTestContext();
     var ctx = new helpers.ServiceTestContext();
 
 
     beforeEach(module('grafana.services'));
     beforeEach(module('grafana.services'));
-    beforeEach(ctx.providePhase());
+    beforeEach(ctx.providePhase(['templateSrv']));
     beforeEach(ctx.createService('InfluxDatasource'));
     beforeEach(ctx.createService('InfluxDatasource'));
     beforeEach(function() {
     beforeEach(function() {
       ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
       ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
@@ -70,6 +70,30 @@ define([
 
 
     });
     });
 
 
+    describe('When issuing annotation query', function() {
+      var results;
+      var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
+                        "+where+time+%3E+now()+-+1h&time_precision=s";
+
+      var range = { from: 'now-1h', to: 'now' };
+      var annotation = { query: 'select title from events.$server where $timeFilter' };
+      var response = [];
+
+      beforeEach(function() {
+        ctx.templateSrv.replace = function(str) {
+          return str.replace('$server', 'backend_01');
+        };
+        ctx.$httpBackend.expect('GET', urlExpected).respond(response);
+        ctx.ds.annotationQuery(annotation, range).then(function(data) { results = data; });
+        ctx.$httpBackend.flush();
+      });
+
+      it('should generate the correct query', function() {
+        ctx.$httpBackend.verifyNoOutstandingExpectation();
+      });
+
+    });
+
   });
   });
 
 
 });
 });

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

@@ -15,6 +15,12 @@ define([
       expect(str).to.be('1.02 hour');
       expect(str).to.be('1.02 hour');
     });
     });
 
 
+    it('should not downscale when value is zero', function() {
+      var str = kbn.msFormat(0, 2);
+      expect(str).to.be('0.00 ms');
+    });
+
+
     it('should translate 365445 as 6.09 min', function() {
     it('should translate 365445 as 6.09 min', function() {
       var str = kbn.msFormat(365445, 2);
       var str = kbn.msFormat(365445, 2);
       expect(str).to.be('6.09 min');
       expect(str).to.be('6.09 min');

+ 35 - 0
src/test/specs/templateSrv-specs.js

@@ -62,6 +62,24 @@ define([
 
 
     });
     });
 
 
+    describe('when checking if a string contains a variable', function() {
+      beforeEach(function() {
+        _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
+        _templateSrv.updateTemplateData();
+      });
+
+      it('should find it with $var syntax', function() {
+        var contains = _templateSrv.containsVariable('this.$test.filters', 'test');
+        expect(contains).to.be(true);
+      });
+
+      it('should find it with [[var]] syntax', function() {
+        var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test');
+        expect(contains).to.be(true);
+      });
+
+    });
+
     describe('updateTemplateData with simple value', function() {
     describe('updateTemplateData with simple value', function() {
       beforeEach(function() {
       beforeEach(function() {
         _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
         _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
@@ -74,6 +92,23 @@ define([
       });
       });
     });
     });
 
 
+    describe('replaceWithText', function() {
+      beforeEach(function() {
+        _templateSrv.init([
+          { name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
+          { name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
+        ]);
+        _templateSrv.setGrafanaVariable('$__auto_interval', '13m');
+        _templateSrv.updateTemplateData();
+      });
+
+      it('should replace with text except for grafanaVariables', function() {
+        var target = _templateSrv.replaceWithText('Server: $server, period: $period');
+        expect(target).to.be('Server: All, period: 13m');
+      });
+    });
+
+
   });
   });
 
 
 });
 });

+ 3 - 15
src/test/specs/templateValuesSrv-specs.js

@@ -10,7 +10,7 @@ define([
     var ctx = new helpers.ServiceTestContext();
     var ctx = new helpers.ServiceTestContext();
 
 
     beforeEach(module('grafana.services'));
     beforeEach(module('grafana.services'));
-    beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv']));
+    beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', "$routeParams"]));
     beforeEach(ctx.createService('templateValuesSrv'));
     beforeEach(ctx.createService('templateValuesSrv'));
 
 
     describe('update interval variable options', function() {
     describe('update interval variable options', function() {
@@ -125,12 +125,12 @@ define([
     describeUpdateVariable('and existing value still exists in options', function(scenario) {
     describeUpdateVariable('and existing value still exists in options', function(scenario) {
       scenario.setup(function() {
       scenario.setup(function() {
         scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
         scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
-        scenario.variable.current = { value: 'backend2'};
+        scenario.variable.current = { text: 'backend2'};
         scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
         scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
       });
       });
 
 
       it('should keep variable value', function() {
       it('should keep variable value', function() {
-        expect(scenario.variable.current.value).to.be('backend2');
+        expect(scenario.variable.current.text).to.be('backend2');
       });
       });
     });
     });
 
 
@@ -182,18 +182,6 @@ define([
       });
       });
     });
     });
 
 
-    describeUpdateVariable('and existing value still exists in options', function(scenario) {
-      scenario.setup(function() {
-        scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
-        scenario.variable.current = { value: 'backend2'};
-        scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
-      });
-
-      it('should keep variable value', function() {
-        expect(scenario.variable.current.value).to.be('backend2');
-      });
-    });
-
     describeUpdateVariable('with include All glob syntax', function(scenario) {
     describeUpdateVariable('with include All glob syntax', function(scenario) {
       scenario.setup(function() {
       scenario.setup(function() {
         scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' };
         scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' };

+ 40 - 1
src/test/specs/timeSrv-specs.js

@@ -11,7 +11,7 @@ define([
     var _dashboard;
     var _dashboard;
 
 
     beforeEach(module('grafana.services'));
     beforeEach(module('grafana.services'));
-    beforeEach(ctx.providePhase());
+    beforeEach(ctx.providePhase(['$routeParams']));
     beforeEach(ctx.createService('timeSrv'));
     beforeEach(ctx.createService('timeSrv'));
 
 
     beforeEach(function() {
     beforeEach(function() {
@@ -35,6 +35,45 @@ define([
       });
       });
     });
     });
 
 
+    describe('init time from url', function() {
+      it('should handle relative times', function() {
+        ctx.$routeParams.from = 'now-2d';
+        ctx.$routeParams.to = 'now';
+        ctx.service.init(_dashboard);
+        var time = ctx.service.timeRange(false);
+        expect(time.from).to.be('now-2d');
+        expect(time.to).to.be('now');
+      });
+
+      it('should handle formated dates', function() {
+        ctx.$routeParams.from = '20140410T052010';
+        ctx.$routeParams.to = '20140520T031022';
+        ctx.service.init(_dashboard);
+        var time = ctx.service.timeRange(true);
+        expect(time.from.getTime()).to.equal(new Date("2014-04-10T05:20:10Z").getTime());
+        expect(time.to.getTime()).to.equal(new Date("2014-05-20T03:10:22Z").getTime());
+      });
+
+      it('should handle formated dates without time', function() {
+        ctx.$routeParams.from = '20140410';
+        ctx.$routeParams.to = '20140520';
+        ctx.service.init(_dashboard);
+        var time = ctx.service.timeRange(true);
+        expect(time.from.getTime()).to.equal(new Date("2014-04-10T00:00:00Z").getTime());
+        expect(time.to.getTime()).to.equal(new Date("2014-05-20T00:00:00Z").getTime());
+      });
+
+      it('should handle epochs', function() {
+        ctx.$routeParams.from = '1410337646373';
+        ctx.$routeParams.to = '1410337665699';
+        ctx.service.init(_dashboard);
+        var time = ctx.service.timeRange(true);
+        expect(time.from.getTime()).to.equal(1410337646373);
+        expect(time.to.getTime()).to.equal(1410337665699);
+      });
+
+    });
+
     describe('setTime', function() {
     describe('setTime', function() {
       it('should return disable refresh for absolute times', function() {
       it('should return disable refresh for absolute times', function() {
         _dashboard.refresh = false;
         _dashboard.refresh = false;

+ 6 - 1
tasks/options/requirejs.js

@@ -1,4 +1,6 @@
 module.exports = function(config,grunt) {
 module.exports = function(config,grunt) {
+  'use strict';
+
   var _c = {
   var _c = {
     build: {
     build: {
       options: {
       options: {
@@ -59,12 +61,15 @@ module.exports = function(config,grunt) {
         'directives/all',
         'directives/all',
         'jquery.flot.pie',
         'jquery.flot.pie',
         'angular-dragdrop',
         'angular-dragdrop',
+        'controllers/all',
+        'routes/all',
+        'components/partials',
       ]
       ]
     }
     }
   ];
   ];
 
 
   var fs = require('fs');
   var fs = require('fs');
-  var panelPath = config.srcDir+'/app/panels'
+  var panelPath = config.srcDir+'/app/panels';
 
 
   // create a module for each directory in src/app/panels/
   // create a module for each directory in src/app/panels/
   fs.readdirSync(panelPath).forEach(function (panelName) {
   fs.readdirSync(panelPath).forEach(function (panelName) {