Browse Source

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

Conflicts:
	src/css/less/grafana.less
	src/test/test-main.js
Torkel Ödegaard 11 years ago
parent
commit
4b382e0faf
100 changed files with 3548 additions and 1959 deletions
  1. 55 4
      CHANGELOG.md
  2. 2 2
      latest.json
  3. 1 1
      package.json
  4. 38 19
      src/app/components/kbn.js
  5. 1 0
      src/app/components/settings.js
  6. 1 2
      src/app/components/timeSeries.js
  7. 3 1
      src/app/controllers/all.js
  8. 37 24
      src/app/controllers/annotationsEditorCtrl.js
  9. 49 36
      src/app/controllers/dashboardCtrl.js
  10. 36 30
      src/app/controllers/dashboardNavCtrl.js
  11. 37 33
      src/app/controllers/graphiteTarget.js
  12. 18 3
      src/app/controllers/influxTargetCtrl.js
  13. 22 0
      src/app/controllers/jsonEditorCtrl.js
  14. 0 1
      src/app/controllers/playlistCtrl.js
  15. 13 1
      src/app/controllers/row.js
  16. 67 22
      src/app/controllers/search.js
  17. 12 2
      src/app/controllers/submenuCtrl.js
  18. 84 0
      src/app/controllers/templateEditorCtrl.js
  19. 12 34
      src/app/dashboards/default.json
  20. 0 12
      src/app/dashboards/empty.json
  21. 4 6
      src/app/dashboards/scripted_async.js
  22. 96 0
      src/app/dashboards/scripted_templated.js
  23. 264 0
      src/app/dashboards/template_vars.json
  24. 9 0
      src/app/directives/addGraphiteFunc.js
  25. 3 0
      src/app/directives/all.js
  26. 3 9
      src/app/directives/bodyClass.js
  27. 2 2
      src/app/directives/bootstrap-tagsinput.js
  28. 84 0
      src/app/directives/dashEditLink.js
  29. 3 2
      src/app/directives/dashUpload.js
  30. 20 12
      src/app/directives/grafanaGraph.js
  31. 3 3
      src/app/directives/grafanaPanel.js
  32. 1 1
      src/app/directives/grafanaVersionCheck.js
  33. 8 8
      src/app/directives/graphiteFuncEditor.js
  34. 134 0
      src/app/directives/graphiteSegment.js
  35. 82 0
      src/app/directives/templateParamSelector.js
  36. 2 2
      src/app/directives/tip.js
  37. 3 56
      src/app/filters/all.js
  38. 0 67
      src/app/panels/annotations/editor.html
  39. 0 12
      src/app/panels/annotations/module.html
  40. 0 39
      src/app/panels/annotations/module.js
  41. 0 50
      src/app/panels/filtering/module.html
  42. 0 104
      src/app/panels/filtering/module.js
  43. 19 10
      src/app/panels/graph/module.html
  44. 15 13
      src/app/panels/graph/module.js
  45. 9 14
      src/app/panels/graph/styleEditor.html
  46. 2 3
      src/app/panels/text/editor.html
  47. 1 1
      src/app/panels/text/module.html
  48. 2 3
      src/app/panels/text/module.js
  49. 73 67
      src/app/panels/timepicker/custom.html
  50. 4 5
      src/app/panels/timepicker/module.html
  51. 14 26
      src/app/panels/timepicker/module.js
  52. 85 0
      src/app/partials/annotations_editor.html
  53. 97 111
      src/app/partials/dashboard.html
  54. 12 3
      src/app/partials/dashboard_topnav.html
  55. 98 93
      src/app/partials/dasheditor.html
  56. 19 0
      src/app/partials/edit_json.html
  57. 103 36
      src/app/partials/graphite/editor.html
  58. 2 2
      src/app/partials/import.html
  59. 2 2
      src/app/partials/influxdb/annotation_editor.html
  60. 184 106
      src/app/partials/influxdb/editor.html
  61. 65 54
      src/app/partials/inspector.html
  62. 3 3
      src/app/partials/opentsdb/editor.html
  63. 0 6
      src/app/partials/paneladd.html
  64. 26 19
      src/app/partials/paneleditor.html
  65. 5 2
      src/app/partials/panelgeneral.html
  66. 60 54
      src/app/partials/playlist.html
  67. 33 24
      src/app/partials/roweditor.html
  68. 78 95
      src/app/partials/search.html
  69. 49 0
      src/app/partials/submenu.html
  70. 170 0
      src/app/partials/templating_editor.html
  71. 1 1
      src/app/routes/dashboard-from-db.js
  72. 3 1
      src/app/services/all.js
  73. 4 6
      src/app/services/annotationsSrv.js
  74. 5 3
      src/app/services/dashboard/dashboardKeyBindings.js
  75. 36 40
      src/app/services/dashboard/dashboardSrv.js
  76. 34 27
      src/app/services/dashboard/dashboardViewStateSrv.js
  77. 7 4
      src/app/services/datasourceSrv.js
  78. 81 25
      src/app/services/elasticsearch/es-datasource.js
  79. 0 98
      src/app/services/filterSrv.js
  80. 56 26
      src/app/services/graphite/gfunc.js
  81. 55 24
      src/app/services/graphite/graphiteDatasource.js
  82. 1 0
      src/app/services/graphite/lexer.js
  83. 19 0
      src/app/services/graphite/parser.js
  84. 67 0
      src/app/services/influxdb/influxQueryBuilder.js
  85. 87 102
      src/app/services/influxdb/influxdbDatasource.js
  86. 1 1
      src/app/services/opentsdb/opentsdbDatasource.js
  87. 8 2
      src/app/services/panelMove.js
  88. 15 13
      src/app/services/panelSrv.js
  89. 93 0
      src/app/services/templateSrv.js
  90. 171 0
      src/app/services/templateValuesSrv.js
  91. 121 0
      src/app/services/timeSrv.js
  92. 8 1
      src/app/services/unsavedChangesSrv.js
  93. 4 1
      src/config.sample.js
  94. 10 9
      src/css/less/bootswatch.dark.less
  95. 3 2
      src/css/less/bootswatch.light.less
  96. 0 2
      src/css/less/console.less
  97. 125 140
      src/css/less/grafana.less
  98. 1 3
      src/css/less/graph.less
  99. 22 76
      src/css/less/overrides.less
  100. 71 0
      src/css/less/panel.less

+ 55 - 4
CHANGELOG.md

@@ -1,16 +1,67 @@
 # 1.8.0 (unreleased)
 # 1.8.0 (unreleased)
 
 
-**New features and improvements**
+**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
+
+# 1.8.0-RC1 (2014-09-12)
+
+**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.
+
+**Filtering/Templating feature overhaul**
+- Filtering renamed to Templating, and filter items to variables
+- Filter editing has gotten its own edit pane with much improved UI and options
+- [Issue #296](https://github.com/grafana/grafana/issues/296). Templating: Can now retrieve variable values from a non-default data source
+- [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
+- [Issue #760](https://github.com/grafana/grafana/issues/760). Templating: Extend template variable syntax to include $variable syntax replacement
+- [Issue #234](https://github.com/grafana/grafana/issues/234). Templating: Interval variable type for time intervals summarize/group by parameter, included "auto" option, and auto step counts option.
+- [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 #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**
+- To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
+- Currently some of these changes are breaking
+- If you used custom condition filter you need to open the graph in edit mode, the editor will update the schema, and the queries should work again
+- If you used a raw query you need to remove the time filter and replace it with $timeFilter (this is done automatically when you switch from query editor to raw query, but old raw queries needs to updated)
+- If you used group by and later removed the group by the graph could break, open in editor and should correct it
+- InfluxDB annotation queries that used [[timeFilter]] should be updated to use $timeFilter syntax instead
+- Might write an upgrade tool to update dashboards automatically, but right now master (1.8) includes the above breaking changes
+
+**InfluxDB query editor enhancements**
+- [Issue #756](https://github.com/grafana/grafana/issues/756). InfluxDB: Add option for fill(0) and fill(null), integrated help in editor for why this option is important when stacking series
+- [Issue #743](https://github.com/grafana/grafana/issues/743). InfluxDB: A group by time option for all queries in graph panel that supports a low limit for auto group by time, very important for stacking and fill(0)
+- The above to enhancements solves the problems associated with stacked bars and lines when points are missing, these issues are solved:
+- [Issue #673](https://github.com/grafana/grafana/issues/673). InfluxDB: stacked bars missing intermediate data points, unless lines also enabled
+- [Issue #674](https://github.com/grafana/grafana/issues/674). InfluxDB: stacked chart ignoring series without latest values
+- [Issue #534](https://github.com/grafana/grafana/issues/534). InfluxDB: No order in stacked bars mode
 
 
+**New features and improvements**
+- [Issue #117](https://github.com/grafana/grafana/issues/117). Graphite: Graphite query builder can now handle functions that multiple series as arguments!
+- [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
+- [Issue #304](https://github.com/grafana/grafana/issues/304). Dashboard: View dashboard json, edit/update any panel using json editor, makes it possible to quickly copy a graph from one dashboard to another.
 - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
 - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
 - [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
 - [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
 - [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
 - [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
 - [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
 - [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 #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)
+- [Issue #733](https://github.com/grafana/grafana/issues/733). Graph: Fix for tooltip current value decimal precision when 'none' axis format was selected
 - [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
 - [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
-- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected. Closes #277
+- [Issue #702](https://github.com/grafana/grafana/issues/702). Graphite: Fix for nonNegativeDerivative function, now possible to not include optional first parameter maxValue
+- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected.
+- [Issue #699](https://github.com/grafana/grafana/issues/699). Dashboard: Fix for bug when adding rows from dashboard settings dialog.
+- [Issue #723](https://github.com/grafana/grafana/issues/723). Dashboard: Fix for hide controls setting not used/initialized on dashboard load
+- [Issue #724](https://github.com/grafana/grafana/issues/724). Dashboard: Fix for zoom out causing right hand "to" range to be set in the future.
 
 
 **Tech**
 **Tech**
 - Upgraded from angularjs 1.1.5 to 1.3 beta 17;
 - Upgraded from angularjs 1.1.5 to 1.3 beta 17;
@@ -22,7 +73,7 @@
 # 1.7.1 (unreleased)
 # 1.7.1 (unreleased)
 
 
 **Fixes**
 **Fixes**
-- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
+- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: Tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
 - [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
 - [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
 
 
 # 1.7.0 (2014-08-11)
 # 1.7.0 (2014-08-11)
@@ -194,7 +245,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.7.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"

+ 38 - 19
src/app/components/kbn.js

@@ -8,25 +8,6 @@ function($, _, moment) {
 
 
   var kbn = {};
   var kbn = {};
 
 
-   /**
-     * Calculate a graph interval
-     *
-     * from::           Date object containing the start time
-     * to::             Date object containing the finish time
-     * size::           Calculate to approximately this many bars
-     * user_interval::  User specified histogram interval
-     *
-     */
-  kbn.calculate_interval = function(from,to,size,user_interval) {
-    if(_.isObject(from)) {
-      from = from.valueOf();
-    }
-    if(_.isObject(to)) {
-      to = to.valueOf();
-    }
-    return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
-  };
-
   kbn.round_interval = function(interval) {
   kbn.round_interval = function(interval) {
     switch (true) {
     switch (true) {
     // 0.5s
     // 0.5s
@@ -131,6 +112,28 @@ function($, _, moment) {
     s: 1
     s: 1
   };
   };
 
 
+  kbn.calculateInterval = function(range, resolution, userInterval) {
+    var lowLimitMs = 1; // 1 millisecond default low limit
+    var intervalMs, lowLimitInterval;
+
+    if (userInterval) {
+      if (userInterval[0] === '>') {
+        lowLimitInterval = userInterval.slice(1);
+        lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
+      }
+      else {
+        return userInterval;
+      }
+    }
+
+    intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
+    if (lowLimitMs > intervalMs) {
+      intervalMs = lowLimitMs;
+    }
+
+    return kbn.secondsToHms(intervalMs / 1000);
+  };
+
   kbn.describe_interval = function (string) {
   kbn.describe_interval = function (string) {
     var matches = string.match(kbn.interval_regex);
     var matches = string.match(kbn.interval_regex);
     if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
     if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
@@ -652,5 +655,21 @@ function($, _, moment) {
     }
     }
   };
   };
 
 
+  kbn.slugifyForUrl = function(str) {
+    return str
+      .toLowerCase()
+      .replace(/[^\w ]+/g,'')
+      .replace(/ +/g,'-');
+  };
+
+  kbn.stringToJsRegex = function(str) {
+    if (str[0] !== '/') {
+      return new RegExp(str);
+    }
+
+    var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+    return new RegExp(match[1], match[2]);
+  };
+
   return kbn;
   return kbn;
 });
 });

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

@@ -14,6 +14,7 @@ function (_, crypto) {
      */
      */
     var defaults = {
     var defaults = {
       datasources                   : {},
       datasources                   : {},
+      window_title_prefix           : 'Grafana - ',
       panels                        : ['graph', 'text'],
       panels                        : ['graph', 'text'],
       plugins                       : {},
       plugins                       : {},
       default_route                 : '/dashboard/file/default.json',
       default_route                 : '/dashboard/file/default.json',

+ 1 - 2
src/app/components/timeSeries.js

@@ -15,8 +15,7 @@ function (_, kbn) {
     if (!aliasOrRegex) { return false; }
     if (!aliasOrRegex) { return false; }
 
 
     if (aliasOrRegex[0] === '/') {
     if (aliasOrRegex[0] === '/') {
-      var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
-      var regex = new RegExp(match[1], match[2]);
+      var regex = kbn.stringToJsRegex(aliasOrRegex);
       return seriesAlias.match(regex) != null;
       return seriesAlias.match(regex) != null;
     }
     }
 
 

+ 3 - 1
src/app/controllers/all.js

@@ -13,5 +13,7 @@ define([
   './playlistCtrl',
   './playlistCtrl',
   './inspectCtrl',
   './inspectCtrl',
   './opentsdbTargetCtrl',
   './opentsdbTargetCtrl',
-  './console-ctrl',
+  './annotationsEditorCtrl',
+  './templateEditorCtrl',
+  './jsonEditorCtrl',
 ], function () {});
 ], function () {});

+ 37 - 24
src/app/panels/annotations/editor.js → src/app/controllers/annotationsEditorCtrl.js

@@ -1,19 +1,14 @@
-/*
-
-*/
 define([
 define([
   'angular',
   'angular',
-  'app',
-  'lodash'
+  'lodash',
+  'jquery'
 ],
 ],
-function (angular, app, _) {
+function (angular, _, $) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.panels.annotations', []);
-  app.useModule(module);
+  var module = angular.module('grafana.controllers');
 
 
   module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
   module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
-
     var annotationDefaults = {
     var annotationDefaults = {
       name: '',
       name: '',
       datasource: null,
       datasource: null,
@@ -25,39 +20,57 @@ function (angular, app, _) {
     };
     };
 
 
     $scope.init = function() {
     $scope.init = function() {
-      $scope.currentAnnotation = angular.copy(annotationDefaults);
-      $scope.currentIsNew = true;
+      $scope.editor = { index: 0 };
       $scope.datasources = datasourceSrv.getAnnotationSources();
       $scope.datasources = datasourceSrv.getAnnotationSources();
+      $scope.annotations = $scope.dashboard.annotations.list;
+      $scope.reset();
 
 
-      if ($scope.datasources.length > 0) {
-        $scope.currentDatasource = $scope.datasources[0];
-      }
+      $scope.$watch('editor.index', function(newVal) {
+        if (newVal !== 2) {
+          $scope.reset();
+        }
+      });
     };
     };
 
 
-    $scope.setDatasource = function() {
-      $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
+    $scope.datasourceChanged = function() {
+      $scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
+      if (!$scope.currentDatasource) {
+        $scope.currentDatasource = $scope.datasources[0];
+      }
     };
     };
 
 
     $scope.edit = function(annotation) {
     $scope.edit = function(annotation) {
       $scope.currentAnnotation = annotation;
       $scope.currentAnnotation = annotation;
       $scope.currentIsNew = false;
       $scope.currentIsNew = false;
-      $scope.currentDatasource = _.findWhere($scope.datasources, { name: annotation.datasource });
+      $scope.datasourceChanged();
 
 
-      if (!$scope.currentDatasource) {
-        $scope.currentDatasource = $scope.datasources[0];
-      }
+      $scope.editor.index = 2;
+      $(".tooltip.in").remove();
     };
     };
 
 
-    $scope.update = function() {
+    $scope.reset = function() {
       $scope.currentAnnotation = angular.copy(annotationDefaults);
       $scope.currentAnnotation = angular.copy(annotationDefaults);
       $scope.currentIsNew = true;
       $scope.currentIsNew = true;
+      $scope.datasourceChanged();
+      $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
+    };
+
+    $scope.update = function() {
+      $scope.reset();
+      $scope.editor.index = 0;
     };
     };
 
 
     $scope.add = function() {
     $scope.add = function() {
-      $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
-      $scope.panel.annotations.push($scope.currentAnnotation);
-      $scope.currentAnnotation = angular.copy(annotationDefaults);
+      $scope.annotations.push($scope.currentAnnotation);
+      $scope.reset();
+      $scope.editor.index = 0;
+    };
+
+    $scope.removeAnnotation = function(annotation) {
+      var index = _.indexOf($scope.annotations, annotation);
+      $scope.annotations.splice(index, 1);
     };
     };
 
 
   });
   });
+
 });
 });

+ 49 - 36
src/app/controllers/dashboardCtrl.js

@@ -11,60 +11,62 @@ function (angular, $, config, _) {
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
   module.controller('DashboardCtrl', function(
   module.controller('DashboardCtrl', function(
-      $scope, $rootScope, dashboardKeybindings,
-      filterSrv, dashboardSrv, dashboardViewStateSrv,
-      panelMoveSrv, timer, $timeout) {
+      $scope,
+      $rootScope,
+      dashboardKeybindings,
+      timeSrv,
+      templateValuesSrv,
+      dashboardSrv,
+      dashboardViewStateSrv,
+      panelMoveSrv,
+      timer,
+      $timeout) {
 
 
     $scope.editor = { index: 0 };
     $scope.editor = { index: 0 };
     $scope.panelNames = config.panels;
     $scope.panelNames = config.panels;
+    var resizeEventTimeout;
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.availablePanels = config.panels;
       $scope.availablePanels = config.panels;
       $scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
       $scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
+      $scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
+      $scope.reset_row();
+      $scope.registerWindowResizeEvent();
+    };
 
 
+    $scope.registerWindowResizeEvent = function() {
       angular.element(window).bind('resize', function() {
       angular.element(window).bind('resize', function() {
-        $timeout(function() {
-          $scope.$broadcast('render');
-        });
+        $timeout.cancel(resizeEventTimeout);
+        resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
       });
       });
-
     };
     };
 
 
     $scope.setupDashboard = function(event, dashboardData) {
     $scope.setupDashboard = function(event, dashboardData) {
-      timer.cancel_all();
-
       $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);
 
 
-      $scope.grafana.style = $scope.dashboard.style;
-
-      $scope.filter = filterSrv;
-      $scope.filter.init($scope.dashboard);
-
-      var panelMove = panelMoveSrv.create($scope.dashboard);
-
-      $scope.panelMoveDrop = panelMove.onDrop;
-      $scope.panelMoveStart = panelMove.onStart;
-      $scope.panelMoveStop = panelMove.onStop;
-      $scope.panelMoveOver = panelMove.onOver;
-      $scope.panelMoveOut = panelMove.onOut;
-
-      window.document.title = 'Grafana - ' + $scope.dashboard.title;
-
-      // start auto refresh
-      if($scope.dashboard.refresh) {
-        $scope.dashboard.set_interval($scope.dashboard.refresh);
-      }
+      // init services
+      timeSrv.init($scope.dashboard);
+      templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
+      panelMoveSrv.init($scope.dashboard, $scope);
 
 
+      $scope.checkFeatureToggles();
       dashboardKeybindings.shortcuts($scope);
       dashboardKeybindings.shortcuts($scope);
 
 
+      $scope.setWindowTitleAndTheme();
+
       $scope.emitAppEvent("dashboard-loaded", $scope.dashboard);
       $scope.emitAppEvent("dashboard-loaded", $scope.dashboard);
     };
     };
 
 
+    $scope.setWindowTitleAndTheme = function() {
+      window.document.title = config.window_title_prefix + $scope.dashboard.title;
+      $scope.grafana.style = $scope.dashboard.style;
+    };
+
     $scope.isPanel = function(obj) {
     $scope.isPanel = function(obj) {
       if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
       if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
         return true;
         return true;
@@ -91,23 +93,34 @@ 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;
       }
       }
     };
     };
 
 
+    $scope.showJsonEditor = function(evt, options) {
+      var editScope = $rootScope.$new();
+      editScope.object = options.object;
+      editScope.updateHandler = options.updateHandler;
+      $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
+    };
+
+    $scope.checkFeatureToggles = function() {
+      $scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable;
+    };
+
     $scope.setEditorTabs = function(panelMeta) {
     $scope.setEditorTabs = function(panelMeta) {
       $scope.editorTabs = ['General','Panel'];
       $scope.editorTabs = ['General','Panel'];
       if(!_.isUndefined(panelMeta.editorTabs)) {
       if(!_.isUndefined(panelMeta.editorTabs)) {

+ 36 - 30
src/app/controllers/dashboardNavCtrl.js

@@ -11,14 +11,13 @@ function (angular, _, moment, config, store) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv) {
+  module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv, timeSrv) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.db = datasourceSrv.getGrafanaDB();
       $scope.db = datasourceSrv.getGrafanaDB();
 
 
-      $scope.onAppEvent('save-dashboard', function() {
-        $scope.saveDashboard();
-      });
+      $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
+      $scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
 
 
       $scope.onAppEvent('zoom-out', function() {
       $scope.onAppEvent('zoom-out', function() {
         $scope.zoom(2);
         $scope.zoom(2);
@@ -57,10 +56,10 @@ function (angular, _, moment, config, store) {
 
 
     $scope.isAdmin = function() {
     $scope.isAdmin = function() {
       if (!config.admin || !config.admin.password) { return true; }
       if (!config.admin || !config.admin.password) { return true; }
-      if (this.passwordCache() === config.admin.password) { return true; }
+      if ($scope.passwordCache() === config.admin.password) { return true; }
 
 
       var password = window.prompt("Admin password", "");
       var password = window.prompt("Admin password", "");
-      this.passwordCache(password);
+      $scope.passwordCache(password);
 
 
       if (password === config.admin.password) { return true; }
       if (password === config.admin.password) { return true; }
 
 
@@ -69,16 +68,22 @@ function (angular, _, moment, config, store) {
       return false;
       return false;
     };
     };
 
 
+    $scope.openSearch = function() {
+      $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
+    };
+
     $scope.saveDashboard = function() {
     $scope.saveDashboard = function() {
-      if (!this.isAdmin()) { return false; }
+      if (!$scope.isAdmin()) { return false; }
 
 
       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', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
 
 
-          $location.search({});
-          $location.path(result.url);
+          if (result.url !== $location.path()) {
+            $location.search({});
+            $location.path(result.url);
+          }
 
 
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
 
 
@@ -87,15 +92,14 @@ function (angular, _, moment, config, store) {
         });
         });
     };
     };
 
 
-    $scope.deleteDashboard = function(id, $event) {
-      $event.stopPropagation();
-
+    $scope.deleteDashboard = function(evt, options) {
       if (!confirm('Are you sure you want to delete dashboard?')) {
       if (!confirm('Are you sure you want to delete dashboard?')) {
         return;
         return;
       }
       }
 
 
-      if (!this.isAdmin()) { return false; }
+      if (!$scope.isAdmin()) { return false; }
 
 
+      var id = options.id;
       $scope.db.deleteDashboard(id).then(function(id) {
       $scope.db.deleteDashboard(id).then(function(id) {
         alertSrv.set('Dashboard Deleted', id + ' has been deleted', 'success', 5000);
         alertSrv.set('Dashboard Deleted', id + ' has been deleted', 'success', 5000);
       }, function() {
       }, function() {
@@ -108,26 +112,24 @@ function (angular, _, moment, config, store) {
       window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
       window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
     };
     };
 
 
-    // function $scope.zoom
-    // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
     $scope.zoom = function(factor) {
     $scope.zoom = function(factor) {
-      var _range = $scope.filter.timeRange();
-      var _timespan = (_range.to.valueOf() - _range.from.valueOf());
-      var _center = _range.to.valueOf() - _timespan/2;
-
-      var _to = (_center + (_timespan*factor)/2);
-      var _from = (_center - (_timespan*factor)/2);
-
-      // If we're not already looking into the future, don't.
-      if(_to > Date.now() && _range.to < Date.now()) {
-        var _offset = _to - Date.now();
-        _from = _from - _offset;
-        _to = Date.now();
+      var range = timeSrv.timeRange();
+
+      var timespan = (range.to.valueOf() - range.from.valueOf());
+      var center = range.to.valueOf() - timespan/2;
+
+      var to = (center + (timespan*factor)/2);
+      var from = (center - (timespan*factor)/2);
+
+      if(to > Date.now() && range.to <= Date.now()) {
+        var offset = to - Date.now();
+        from = from - offset;
+        to = Date.now();
       }
       }
 
 
-      $scope.filter.setTime({
-        from:moment.utc(_from).toDate(),
-        to:moment.utc(_to).toDate(),
+      timeSrv.setTime({
+        from: moment.utc(from).toDate(),
+        to: moment.utc(to).toDate(),
       });
       });
     };
     };
 
 
@@ -135,6 +137,10 @@ function (angular, _, moment, config, store) {
       $scope.grafana.style = $scope.dashboard.style;
       $scope.grafana.style = $scope.dashboard.style;
     };
     };
 
 
+    $scope.editJson = function() {
+      $scope.emitAppEvent('show-json-editor', { object: $scope.dashboard });
+    };
+
     $scope.openSaveDropdown = function() {
     $scope.openSaveDropdown = function() {
       $scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
       $scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
       $scope.saveDropdownOpened = true;
       $scope.saveDropdownOpened = true;

+ 37 - 33
src/app/controllers/graphiteTarget.js

@@ -9,11 +9,13 @@ function (angular, _, config, gfunc, Parser) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
+  var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
 
 
-  module.controller('GraphiteTargetCtrl', function($scope, $sce) {
+  module.controller('GraphiteTargetCtrl', function($scope, $sce, templateSrv) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.target.target = $scope.target.target || '';
       $scope.target.target = $scope.target.target || '';
+      $scope.targetLetters = targetLetters;
 
 
       parseTarget();
       parseTarget();
     };
     };
@@ -52,6 +54,13 @@ function (angular, _, config, gfunc, Parser) {
       checkOtherSegments($scope.segments.length - 1);
       checkOtherSegments($scope.segments.length - 1);
     }
     }
 
 
+    function addFunctionParameter(func, value, index, shiftBack) {
+      if (shiftBack) {
+        index = Math.max(index - 1, 0);
+      }
+      func.params[index] = value;
+    }
+
     function parseTargeRecursive(astNode, func, index) {
     function parseTargeRecursive(astNode, func, index) {
       if (astNode === null) {
       if (astNode === null) {
         return null;
         return null;
@@ -59,7 +68,7 @@ function (angular, _, config, gfunc, Parser) {
 
 
       switch(astNode.type) {
       switch(astNode.type) {
       case 'function':
       case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name);
+        var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
 
 
         _.each(astNode.params, function(param, index) {
         _.each(astNode.params, function(param, index) {
           parseTargeRecursive(param, innerFunc, index);
           parseTargeRecursive(param, innerFunc, index);
@@ -69,24 +78,23 @@ function (angular, _, config, gfunc, Parser) {
         $scope.functions.push(innerFunc);
         $scope.functions.push(innerFunc);
         break;
         break;
 
 
+      case 'series-ref':
+        addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
+        break;
       case 'string':
       case 'string':
       case 'number':
       case 'number':
         if ((index-1) >= func.def.params.length) {
         if ((index-1) >= func.def.params.length) {
           throw { message: 'invalid number of parameters to method ' + func.def.name };
           throw { message: 'invalid number of parameters to method ' + func.def.name };
         }
         }
-
-        if (index === 0) {
-          func.params[index] = astNode.value;
-        }
-        else {
-          func.params[index - 1] = astNode.value;
-        }
-
+        addFunctionParameter(func, astNode.value, index, true);
         break;
         break;
-
       case 'metric':
       case 'metric':
         if ($scope.segments.length > 0) {
         if ($scope.segments.length > 0) {
-          throw { message: 'Multiple metric params not supported, use text editor.' };
+          if (astNode.segments.length !== 1) {
+            throw { message: 'Multiple metric params not supported, use text editor.' };
+          }
+          addFunctionParameter(func, astNode.segments[0].value, index, true);
+          break;
         }
         }
 
 
         $scope.segments = _.map(astNode.segments, function(segment) {
         $scope.segments = _.map(astNode.segments, function(segment) {
@@ -110,11 +118,13 @@ function (angular, _, config, gfunc, Parser) {
       }
       }
 
 
       var path = getSegmentPathUpTo(fromIndex + 1);
       var path = getSegmentPathUpTo(fromIndex + 1);
-      return $scope.datasource.metricFindQuery($scope.filter, path)
+      return $scope.datasource.metricFindQuery(path)
         .then(function(segments) {
         .then(function(segments) {
           if (segments.length === 0) {
           if (segments.length === 0) {
-            $scope.segments = $scope.segments.splice(0, fromIndex);
-            $scope.segments.push(new MetricSegment('select metric'));
+            if (path !== '') {
+              $scope.segments = $scope.segments.splice(0, fromIndex);
+              $scope.segments.push(new MetricSegment('select metric'));
+            }
             return;
             return;
           }
           }
           if (segments[0].expandable) {
           if (segments[0].expandable) {
@@ -144,19 +154,18 @@ function (angular, _, config, gfunc, Parser) {
     $scope.getAltSegments = function (index) {
     $scope.getAltSegments = function (index) {
       $scope.altSegments = [];
       $scope.altSegments = [];
 
 
-      var query = index === 0 ?
-        '*' : getSegmentPathUpTo(index) + '.*';
+      var query = index === 0 ?  '*' : getSegmentPathUpTo(index) + '.*';
 
 
-      return $scope.datasource.metricFindQuery($scope.filter, query)
+      return $scope.datasource.metricFindQuery(query)
         .then(function(segments) {
         .then(function(segments) {
           $scope.altSegments = _.map(segments, function(segment) {
           $scope.altSegments = _.map(segments, function(segment) {
             return new MetricSegment({ value: segment.text, expandable: segment.expandable });
             return new MetricSegment({ value: segment.text, expandable: segment.expandable });
           });
           });
 
 
-          _.each($scope.filter.templateParameters, function(templateParameter) {
+          _.each(templateSrv.variables, function(variable) {
             $scope.altSegments.unshift(new MetricSegment({
             $scope.altSegments.unshift(new MetricSegment({
               type: 'template',
               type: 'template',
-              value: '[[' + templateParameter.name + ']]',
+              value: '$' + variable.name,
               expandable: true,
               expandable: true,
             }));
             }));
           });
           });
@@ -168,17 +177,14 @@ function (angular, _, config, gfunc, Parser) {
         });
         });
     };
     };
 
 
-    $scope.setSegment = function (altIndex, segmentIndex) {
+    $scope.segmentValueChanged = function (segment, segmentIndex) {
       delete $scope.parserError;
       delete $scope.parserError;
 
 
-      $scope.segments[segmentIndex].value = $scope.altSegments[altIndex].value;
-      $scope.segments[segmentIndex].html = $scope.altSegments[altIndex].html;
-
       if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
       if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
         $scope.functions = [];
         $scope.functions = [];
       }
       }
 
 
-      if ($scope.altSegments[altIndex].expandable) {
+      if (segment.expandable) {
         return checkOtherSegments(segmentIndex + 1)
         return checkOtherSegments(segmentIndex + 1)
           .then(function () {
           .then(function () {
             setSegmentFocus(segmentIndex + 1);
             setSegmentFocus(segmentIndex + 1);
@@ -219,13 +225,17 @@ function (angular, _, config, gfunc, Parser) {
     };
     };
 
 
     $scope.addFunction = function(funcDef) {
     $scope.addFunction = function(funcDef) {
-      var newFunc = gfunc.createFuncInstance(funcDef);
+      var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
       newFunc.added = true;
       newFunc.added = true;
       $scope.functions.push(newFunc);
       $scope.functions.push(newFunc);
 
 
       $scope.moveAliasFuncLast();
       $scope.moveAliasFuncLast();
       $scope.smartlyHandleNewAliasByNode(newFunc);
       $scope.smartlyHandleNewAliasByNode(newFunc);
 
 
+      if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') {
+        $scope.segments = [];
+      }
+
       if (!newFunc.params.length && newFunc.added) {
       if (!newFunc.params.length && newFunc.added) {
         $scope.targetChanged();
         $scope.targetChanged();
       }
       }
@@ -287,13 +297,7 @@ function (angular, _, config, gfunc, Parser) {
       this.value = options.value;
       this.value = options.value;
       this.type = options.type;
       this.type = options.type;
       this.expandable = options.expandable;
       this.expandable = options.expandable;
-
-      if (options.type === 'template') {
-        this.html = $sce.trustAsHtml(options.value);
-      }
-      else {
-        this.html = $sce.trustAsHtml(this.value);
-      }
+      this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
     }
     }
 
 
   });
   });

+ 18 - 3
src/app/controllers/influxTargetCtrl.js

@@ -11,8 +11,23 @@ function (angular) {
   module.controller('InfluxTargetCtrl', function($scope, $timeout) {
   module.controller('InfluxTargetCtrl', function($scope, $timeout) {
 
 
     $scope.init = function() {
     $scope.init = function() {
-      $scope.target.function = $scope.target.function || 'mean';
-      $scope.target.column = $scope.target.column || 'value';
+      var target = $scope.target;
+
+      target.function = target.function || 'mean';
+      target.column = target.column || 'value';
+
+      // backward compatible correction of schema
+      if (target.condition_value) {
+        target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
+        delete target.condition_key;
+        delete target.condition_op;
+        delete target.condition_value;
+      }
+
+      if (target.groupby_field_add === false) {
+        target.groupby_field = '';
+        delete target.groupby_field_add;
+      }
 
 
       $scope.rawQuery = false;
       $scope.rawQuery = false;
 
 
@@ -24,7 +39,7 @@ function (angular) {
       ];
       ];
 
 
       $scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
       $scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
-      $scope.oldSeries = $scope.target.series;
+      $scope.oldSeries = target.series;
       $scope.$on('typeahead-updated', function() {
       $scope.$on('typeahead-updated', function() {
         $timeout($scope.get_data);
         $timeout($scope.get_data);
       });
       });

+ 22 - 0
src/app/controllers/jsonEditorCtrl.js

@@ -0,0 +1,22 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('JsonEditorCtrl', function($scope) {
+
+    $scope.json = angular.toJson($scope.object, true);
+    $scope.canUpdate = $scope.updateHandler !== void 0;
+
+    $scope.update = function () {
+      var newObject = angular.fromJson($scope.json);
+      $scope.updateHandler(newObject, $scope.object);
+    };
+
+  });
+
+});

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

@@ -13,7 +13,6 @@ function (angular, _, config) {
     $scope.init = function() {
     $scope.init = function() {
       $scope.timespan = config.playlist_timespan;
       $scope.timespan = config.playlist_timespan;
       $scope.loadFavorites();
       $scope.loadFavorites();
-      $scope.$on('modal-opened', $scope.loadFavorites);
     };
     };
 
 
     $scope.loadFavorites = function() {
     $scope.loadFavorites = function() {

+ 13 - 1
src/app/controllers/row.js

@@ -13,7 +13,6 @@ function (angular, app, _) {
       title: "Row",
       title: "Row",
       height: "150px",
       height: "150px",
       collapse: false,
       collapse: false,
-      editable: true,
       panels: [],
       panels: [],
     };
     };
 
 
@@ -76,6 +75,19 @@ function (angular, app, _) {
       }
       }
     };
     };
 
 
+    $scope.replacePanel = function(newPanel, oldPanel) {
+      var row = $scope.row;
+      var index = _.indexOf(row.panels, oldPanel);
+      row.panels.splice(index, 1);
+
+      // adding it back needs to be done in next digest
+      $timeout(function() {
+        newPanel.id = oldPanel.id;
+        newPanel.span = oldPanel.span;
+        row.panels.splice(index, 0, newPanel);
+      });
+    };
+
     $scope.duplicatePanel = function(panel, row) {
     $scope.duplicatePanel = function(panel, row) {
       $scope.dashboard.duplicatePanel(panel, row || $scope.row);
       $scope.dashboard.duplicatePanel(panel, row || $scope.row);
     };
     };

+ 67 - 22
src/app/controllers/search.js

@@ -9,7 +9,7 @@ function (angular, _, config, $) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, datasourceSrv) {
+  module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, datasourceSrv, $timeout) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.giveSearchFocus = 0;
       $scope.giveSearchFocus = 0;
@@ -17,18 +17,25 @@ 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.onAppEvent('open-search', $scope.openSearch);
+      $scope.currentSearchId = 0;
+
+      $timeout(function() {
+        $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
+        $scope.query.query = 'title:';
+        $scope.search();
+      }, 100);
+
     };
     };
 
 
     $scope.keyDown = function (evt) {
     $scope.keyDown = function (evt) {
       if (evt.keyCode === 27) {
       if (evt.keyCode === 27) {
-        $element.find('.dropdown-toggle').dropdown('toggle');
+        $scope.emitAppEvent('hide-dash-editor');
       }
       }
       if (evt.keyCode === 40) {
       if (evt.keyCode === 40) {
-        $scope.selectedIndex++;
+        $scope.moveSelection(1);
       }
       }
       if (evt.keyCode === 38) {
       if (evt.keyCode === 38) {
-        $scope.selectedIndex--;
+        $scope.moveSelection(-1);
       }
       }
       if (evt.keyCode === 13) {
       if (evt.keyCode === 13) {
         if ($scope.tagsOnly) {
         if ($scope.tagsOnly) {
@@ -50,6 +57,10 @@ function (angular, _, config, $) {
       }
       }
     };
     };
 
 
+    $scope.moveSelection = function(direction) {
+      $scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0);
+    };
+
     $scope.goToDashboard = function(id) {
     $scope.goToDashboard = function(id) {
       $location.path("/dashboard/db/" + id);
       $location.path("/dashboard/db/" + id);
     };
     };
@@ -65,11 +76,22 @@ 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;
+          $scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length;
         });
         });
     };
     };
 
 
@@ -83,8 +105,7 @@ function (angular, _, config, $) {
       }
       }
     };
     };
 
 
-    $scope.showTags = function(evt) {
-      evt.stopPropagation();
+    $scope.showTags = function() {
       $scope.tagsOnly = !$scope.tagsOnly;
       $scope.tagsOnly = !$scope.tagsOnly;
       $scope.query.query = $scope.tagsOnly ? "tags!:" : "";
       $scope.query.query = $scope.tagsOnly ? "tags!:" : "";
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
@@ -94,20 +115,13 @@ function (angular, _, config, $) {
 
 
     $scope.search = function() {
     $scope.search = function() {
       $scope.showImport = false;
       $scope.showImport = false;
-      $scope.selectedIndex = -1;
-
+      $scope.selectedIndex = 0;
       $scope.searchDashboards($scope.query.query);
       $scope.searchDashboards($scope.query.query);
     };
     };
 
 
-    $scope.openSearch = function (evt) {
-      if (evt) {
-        $element.next().find('.dropdown-toggle').dropdown('toggle');
-      }
-
-      $scope.searchOpened = true;
-      $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
-      $scope.query.query = 'title:';
-      $scope.search();
+    $scope.deleteDashboard = function(id, evt) {
+      evt.stopPropagation();
+      $scope.emitAppEvent('delete-dashboard', { id: id });
     };
     };
 
 
     $scope.addMetricToCurrentDashboard = function (metricId) {
     $scope.addMetricToCurrentDashboard = function (metricId) {
@@ -126,8 +140,7 @@ function (angular, _, config, $) {
       });
       });
     };
     };
 
 
-    $scope.toggleImport = function ($event) {
-      $event.stopPropagation();
+    $scope.toggleImport = function () {
       $scope.showImport = !$scope.showImport;
       $scope.showImport = !$scope.showImport;
     };
     };
 
 
@@ -139,16 +152,48 @@ function (angular, _, config, $) {
 
 
   module.directive('xngFocus', function() {
   module.directive('xngFocus', function() {
     return function(scope, element, attrs) {
     return function(scope, element, attrs) {
-      $(element).click(function(e) {
+      element.click(function(e) {
         e.stopPropagation();
         e.stopPropagation();
       });
       });
 
 
       scope.$watch(attrs.xngFocus,function (newValue) {
       scope.$watch(attrs.xngFocus,function (newValue) {
+        if (!newValue) {
+          return;
+        }
         setTimeout(function() {
         setTimeout(function() {
-          newValue && element.focus();
+          element.focus();
+          var pos = element.val().length * 2;
+          element[0].setSelectionRange(pos, pos);
         }, 200);
         }, 200);
       },true);
       },true);
     };
     };
   });
   });
 
 
+  module.directive('tagColorFromName', function() {
+
+    function djb2(str) {
+      var hash = 5381;
+      for (var i = 0; i < str.length; i++) {
+        hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
+      }
+      return hash;
+    }
+
+    return function (scope, element) {
+      var name = _.isString(scope.tag) ? scope.tag : scope.tag.term;
+      var hash = djb2(name.toLowerCase());
+      var colors = [
+        "#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
+        "#508642","#447EBC","#C15C17","#890F02","#757575",
+        "#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
+        "#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
+        "#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
+        "#58140C","#052B51","#511749","#3F2B5B",
+      ];
+      var color = colors[Math.abs(hash % colors.length)];
+      element.css("background-color", color);
+    };
+
+  });
+
 });
 });

+ 12 - 2
src/app/controllers/submenuCtrl.js

@@ -8,7 +8,7 @@ function (angular, app, _) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('SubmenuCtrl', function($scope) {
+  module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv) {
     var _d = {
     var _d = {
       enable: true
       enable: true
     };
     };
@@ -18,10 +18,20 @@ function (angular, app, _) {
     $scope.init = function() {
     $scope.init = function() {
       $scope.panel = $scope.pulldown;
       $scope.panel = $scope.pulldown;
       $scope.row = $scope.pulldown;
       $scope.row = $scope.pulldown;
+      $scope.variables = $scope.dashboard.templating.list;
+    };
+
+    $scope.disableAnnotation = function (annotation) {
+      annotation.enable = !annotation.enable;
+      $rootScope.$broadcast('refresh');
+    };
+
+    $scope.setVariableValue = function(param, option) {
+      templateValuesSrv.setVariableValue(param, option);
     };
     };
 
 
     $scope.init();
     $scope.init();
 
 
   });
   });
 
 
-});
+});

+ 84 - 0
src/app/controllers/templateEditorCtrl.js

@@ -0,0 +1,84 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv, alertSrv) {
+
+    var replacementDefaults = {
+      type: 'query',
+      datasource: null,
+      refresh_on_load: false,
+      name: '',
+      options: [],
+      includeAll: false,
+      allFormat: 'glob',
+    };
+
+    $scope.init = function() {
+      $scope.editor = { index: 0 };
+      $scope.datasources = datasourceSrv.getMetricSources();
+      $scope.variables = templateSrv.variables;
+      $scope.reset();
+
+      $scope.$watch('editor.index', function(index) {
+        if ($scope.currentIsNew === false && index === 1) {
+          $scope.reset();
+        }
+      });
+    };
+
+    $scope.add = function() {
+      $scope.variables.push($scope.current);
+      $scope.update();
+    };
+
+    $scope.runQuery = function() {
+      return templateValuesSrv.updateOptions($scope.current).then(function() {
+      }, function(err) {
+        alertSrv.set('Templating', 'Failed to run query for variable values: ' + err.message, 'error');
+      });
+    };
+
+    $scope.edit = function(variable) {
+      $scope.current = variable;
+      $scope.currentIsNew = false;
+      $scope.editor.index = 2;
+
+      if ($scope.current.datasource === void 0) {
+        $scope.current.datasource = null;
+        $scope.current.type = 'query';
+        $scope.current.allFormat = 'Glob';
+      }
+    };
+
+    $scope.update = function() {
+      $scope.runQuery().then(function() {
+        $scope.reset();
+        $scope.editor.index = 0;
+      });
+    };
+
+    $scope.reset = function() {
+      $scope.currentIsNew = true;
+      $scope.current = angular.copy(replacementDefaults);
+    };
+
+    $scope.typeChanged = function () {
+      if ($scope.current.type === 'interval') {
+        $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
+      }
+    };
+
+    $scope.removeVariable = function(variable) {
+      var index = _.indexOf($scope.variables, variable);
+      $scope.variables.splice(index, 1);
+    };
+
+  });
+
+});

+ 12 - 34
src/app/dashboards/default.json

@@ -8,64 +8,56 @@
     {
     {
       "title": "New row",
       "title": "New row",
       "height": "150px",
       "height": "150px",
-      "editable": true,
       "collapse": false,
       "collapse": false,
-      "collapsable": true,
+      "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"
         }
         }
-      ],
-      "notice": false
+      ]
     },
     },
     {
     {
       "title": "Welcome to Grafana",
       "title": "Welcome to Grafana",
       "height": "210px",
       "height": "210px",
-      "editable": true,
       "collapse": false,
       "collapse": false,
-      "collapsable": true,
+      "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",
           "style": {},
           "style": {},
           "title": "Tips & Shortcuts"
           "title": "Tips & Shortcuts"
         }
         }
-      ],
-      "notice": false
+      ]
     },
     },
     {
     {
       "title": "test",
       "title": "test",
       "height": "250px",
       "height": "250px",
       "editable": true,
       "editable": true,
       "collapse": false,
       "collapse": false,
-      "collapsable": true,
       "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,
@@ -132,27 +124,13 @@
             "enable": false
             "enable": false
           }
           }
         }
         }
-      ],
-      "notice": false
-    }
-  ],
-  "pulldowns": [
-    {
-      "type": "filtering",
-      "collapse": false,
-      "notice": false,
-      "enable": false
-    },
-    {
-      "type": "annotations",
-      "enable": false
+      ]
     }
     }
   ],
   ],
   "nav": [
   "nav": [
     {
     {
       "type": "timepicker",
       "type": "timepicker",
       "collapse": false,
       "collapse": false,
-      "notice": false,
       "enable": true,
       "enable": true,
       "status": "Stable",
       "status": "Stable",
       "time_options": [
       "time_options": [
@@ -188,5 +166,5 @@
   "templating": {
   "templating": {
     "list": []
     "list": []
   },
   },
-  "version": 2
-}
+  "version": 5
+}

+ 0 - 12
src/app/dashboards/empty.json

@@ -17,22 +17,10 @@
     }
     }
   ],
   ],
   "editable": true,
   "editable": true,
-  "failover": false,
-  "panel_hints": true,
   "style": "dark",
   "style": "dark",
-  "pulldowns": [
-    {
-      "type": "filtering",
-      "collapse": false,
-      "notice": false,
-      "enable": false
-    }
-  ],
   "nav": [
   "nav": [
     {
     {
       "type": "timepicker",
       "type": "timepicker",
-      "collapse": false,
-      "notice": false,
       "enable": true,
       "enable": true,
       "status": "Stable",
       "status": "Stable",
       "time_options": [
       "time_options": [

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

+ 96 - 0
src/app/dashboards/scripted_templated.js

@@ -0,0 +1,96 @@
+/* global _ */
+
+/*
+ * Complex scripted dashboard
+ * This script generates a dashboard object that Grafana can load. It also takes a number of user
+ * supplied URL parameters (int ARGS variable)
+ *
+ * Return a dashboard object, or a function
+ *
+ * For async scripts, return a function, this function must take a single callback function as argument,
+ * call this callback function with the dashboard object (look at scripted_async.js for an example)
+ */
+
+'use strict';
+
+// accessable variables in this scope
+var window, document, ARGS, $, jQuery, moment, kbn;
+
+// Setup some variables
+var dashboard, timspan;
+
+// All url parameters are available via the ARGS object
+var ARGS;
+
+// Set a default timespan if one isn't specified
+timspan = '1d';
+
+// Intialize a skeleton with nothing but a rows array and service object
+dashboard = {
+  rows : [],
+};
+
+// Set a title
+dashboard.title = 'Scripted dash';
+dashboard.time = {
+  from: "now-" + (ARGS.from || timspan),
+  to: "now"
+};
+dashboard.templating = {
+  enable: true,
+  list: [
+    {
+      name: 'test',
+      query: 'apps.backend.*',
+      refresh: true,
+      options: [],
+      current: null,
+    },
+    {
+      name: 'test2',
+      query: '*',
+      refresh: true,
+      options: [],
+      current: null,
+    }
+  ]
+};
+
+var rows = 1;
+var seriesName = 'argName';
+
+if(!_.isUndefined(ARGS.rows)) {
+  rows = parseInt(ARGS.rows, 10);
+}
+
+if(!_.isUndefined(ARGS.name)) {
+  seriesName = ARGS.name;
+}
+
+for (var i = 0; i < rows; i++) {
+
+  dashboard.rows.push({
+    title: 'Chart',
+    height: '300px',
+    panels: [
+      {
+        title: 'Events',
+        type: 'graph',
+        span: 12,
+        fill: 1,
+        linewidth: 2,
+        targets: [
+          {
+            'target': "randomWalk('" + seriesName + "')"
+          },
+          {
+            'target': "randomWalk('[[test2]]')"
+          }
+        ],
+      }
+    ]
+  });
+}
+
+
+return 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
+}

+ 9 - 0
src/app/directives/addGraphiteFunc.js

@@ -38,6 +38,15 @@ function (angular, app, _, $, gfunc) {
             items: 10,
             items: 10,
             updater: function (value) {
             updater: function (value) {
               var funcDef = gfunc.getFuncDef(value);
               var funcDef = gfunc.getFuncDef(value);
+              if (!funcDef) {
+                // try find close match
+                value = value.toLowerCase();
+                funcDef = _.find(allFunctions, function(funcName) {
+                  return funcName.toLowerCase().indexOf(value) === 0;
+                });
+
+                if (!funcDef) { return; }
+              }
 
 
               $scope.$apply(function() {
               $scope.$apply(function() {
                 $scope.addFunction(funcDef);
                 $scope.addFunction(funcDef);

+ 3 - 0
src/app/directives/all.js

@@ -4,6 +4,7 @@ define([
   './grafanaPanel',
   './grafanaPanel',
   './grafanaSimplePanel',
   './grafanaSimplePanel',
   './ngBlur',
   './ngBlur',
+  './dashEditLink',
   './ngModelOnBlur',
   './ngModelOnBlur',
   './tip',
   './tip',
   './confirmClick',
   './confirmClick',
@@ -14,6 +15,8 @@ define([
   './bodyClass',
   './bodyClass',
   './addGraphiteFunc',
   './addGraphiteFunc',
   './graphiteFuncEditor',
   './graphiteFuncEditor',
+  './templateParamSelector',
+  './graphiteSegment',
   './grafanaVersionCheck',
   './grafanaVersionCheck',
   './influxdbFuncEditor'
   './influxdbFuncEditor'
 ], function () {});
 ], function () {});

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

@@ -3,7 +3,7 @@ define([
   'app',
   'app',
   'lodash'
   'lodash'
 ],
 ],
-function (angular, app, _) {
+function (angular) {
   'use strict';
   'use strict';
 
 
   angular
   angular
@@ -12,20 +12,14 @@ function (angular, app, _) {
       return {
       return {
         link: function($scope, elem) {
         link: function($scope, elem) {
 
 
-          var lastPulldownVal;
           var lastHideControlsVal;
           var lastHideControlsVal;
 
 
-          $scope.$watchCollection('dashboard.pulldowns', function() {
+          $scope.$watch('submenuEnabled', function() {
             if (!$scope.dashboard) {
             if (!$scope.dashboard) {
               return;
               return;
             }
             }
 
 
-            var panel = _.find($scope.dashboard.pulldowns, function(pulldown) { return pulldown.enable; });
-            var panelEnabled = panel ? panel.enable : false;
-            if (lastPulldownVal !== panelEnabled) {
-              elem.toggleClass('submenu-controls-visible', panelEnabled);
-              lastPulldownVal = panelEnabled;
-            }
+            elem.toggleClass('submenu-controls-visible', $scope.submenuEnabled);
           });
           });
 
 
           $scope.$watch('dashboard.hideControls', function() {
           $scope.$watch('dashboard.hideControls', function() {

+ 2 - 2
src/app/directives/bootstrap-tagsinput.js

@@ -102,7 +102,7 @@ function (angular, $) {
           var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
           var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
             '<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
             '<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
               (item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
               (item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
-              (item.configModal ? ' config-modal="' + item.configModal + '"' : "") +
+              (item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
               '>' + (item.text || '') + '</a>';
               '>' + (item.text || '') + '</a>';
 
 
           if (item.submenu && item.submenu.length) {
           if (item.submenu && item.submenu.length) {
@@ -131,4 +131,4 @@ function (angular, $) {
         }
         }
       };
       };
     });
     });
-});
+});

+ 84 - 0
src/app/directives/dashEditLink.js

@@ -0,0 +1,84 @@
+define([
+  'angular',
+  'jquery'
+],
+function (angular, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('dashEditorLink', function($timeout) {
+      return {
+        restrict: 'A',
+        link: function(scope, elem, attrs) {
+          var partial = attrs.dashEditorLink;
+
+          elem.bind('click',function() {
+            $timeout(function() {
+              var editorScope = attrs.editorScope === 'isolated' ? null : scope;
+              scope.emitAppEvent('show-dash-editor', { src: partial, scope: editorScope });
+            });
+          });
+        }
+      };
+    });
+
+  angular
+    .module('grafana.directives')
+    .directive('dashEditorView', function($compile) {
+      return {
+        restrict: 'A',
+        link: function(scope, elem) {
+          var editorScope;
+          var lastEditor;
+
+          function hideScrollbars(value) {
+            if (value) {
+              document.documentElement.style.overflow = 'hidden';  // firefox, chrome
+              document.body.scroll = "no"; // ie only
+            } else {
+              document.documentElement.style.overflow = 'auto';
+              document.body.scroll = "yes";
+            }
+          }
+
+          function hideEditorPane() {
+            hideScrollbars(false);
+            if (editorScope) { editorScope.dismiss(); }
+          }
+
+          scope.onAppEvent("dashboard-loaded", hideEditorPane);
+          scope.onAppEvent('hide-dash-editor', hideEditorPane);
+
+          scope.onAppEvent('show-dash-editor', function(evt, payload) {
+            hideEditorPane();
+
+            if (lastEditor === payload.src) { return; }
+
+            scope.exitFullscreen();
+
+            lastEditor = payload.src;
+            editorScope = payload.scope ? payload.scope.$new() : scope.$new();
+
+            editorScope.dismiss = function() {
+              editorScope.$destroy();
+              elem.empty();
+              lastEditor = null;
+              editorScope = null;
+              hideScrollbars(false);
+            };
+
+            // hide page scrollbars while edit pane is visible
+            hideScrollbars(true);
+
+            var src = "'" + payload.src + "'";
+            var view = $('<div class="dashboard-edit-view" ng-include="' + src + '"></div>');
+            elem.append(view);
+            $compile(elem.contents())(editorScope);
+          });
+
+        }
+      };
+    });
+
+});

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

@@ -15,8 +15,9 @@ function (angular) {
           var readerOnload = function() {
           var readerOnload = function() {
             return function(e) {
             return function(e) {
               var dashboard = JSON.parse(e.target.result);
               var dashboard = JSON.parse(e.target.result);
-              scope.emitAppEvent('setup-dashboard', dashboard);
-              scope.$apply();
+              scope.$apply(function() {
+                scope.emitAppEvent('setup-dashboard', dashboard);
+              });
             };
             };
           };
           };
           for (var i = 0, f; f = files[i]; i++) {
           for (var i = 0, f; f = files[i]; i++) {

+ 20 - 12
src/app/directives/grafanaGraph.js

@@ -10,12 +10,12 @@ function (angular, $, kbn, moment, _) {
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
 
 
-  module.directive('grafanaGraph', function($rootScope) {
+  module.directive('grafanaGraph', function($rootScope, timeSrv) {
     return {
     return {
       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();
           }
           }
         }
         }
 
 
@@ -355,7 +363,7 @@ function (angular, $, kbn, moment, _) {
               value = item.datapoint[1];
               value = item.datapoint[1];
             }
             }
 
 
-            value = kbn.getFormatFunction(format, 2)(value);
+            value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis);
             timestamp = dashboard.formatDate(item.datapoint[0]);
             timestamp = dashboard.formatDate(item.datapoint[0]);
 
 
             $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
             $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
@@ -416,7 +424,7 @@ function (angular, $, kbn, moment, _) {
 
 
         elem.bind("plotselected", function (event, ranges) {
         elem.bind("plotselected", function (event, ranges) {
           scope.$apply(function() {
           scope.$apply(function() {
-            scope.filter.setTime({
+            timeSrv.setTime({
               from  : moment.utc(ranges.xaxis.from).toDate(),
               from  : moment.utc(ranges.xaxis.from).toDate(),
               to    : moment.utc(ranges.xaxis.to).toDate(),
               to    : moment.utc(ranges.xaxis.to).toDate(),
             });
             });

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

@@ -18,8 +18,8 @@ function (angular, $) {
        '<div class="row-fluid panel-extra">' +
        '<div class="row-fluid panel-extra">' +
           '<div class="panel-extra-container">' +
           '<div class="panel-extra-container">' +
             '<span class="alert-error panel-error small pointer"' +
             '<span class="alert-error panel-error small pointer"' +
-                  'config-modal="app/partials/inspector.html" ng-if="panel.error">' +
-              '<span data-placement="right" bs-tooltip="panel.error">' +
+                  'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' +
+              '<span data-placement="right" bs-tooltip="panelMeta.error">' +
               '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
               '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
               '</span>' +
               '</span>' +
             '</span>' +
             '</span>' +
@@ -40,7 +40,7 @@ function (angular, $) {
                 'onStop:\'panelMoveStop\''+
                 'onStop:\'panelMoveStop\''+
                 '}"  ng-model="panel" ' +
                 '}"  ng-model="panel" ' +
                 '>' +
                 '>' +
-                '{{panel.title || "No title"}}' +
+                '{{panel.title | interpolateTemplateVars}}' +
               '</span>' +
               '</span>' +
             '</span>'+
             '</span>'+
 
 

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

@@ -30,4 +30,4 @@ function (angular) {
         }
         }
       };
       };
     });
     });
-});
+});

+ 8 - 8
src/app/directives/graphiteFuncEditor.js

@@ -8,7 +8,7 @@ function (angular, _, $) {
 
 
   angular
   angular
     .module('grafana.directives')
     .module('grafana.directives')
-    .directive('graphiteFuncEditor', function($compile) {
+    .directive('graphiteFuncEditor', function($compile, templateSrv) {
 
 
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var paramTemplate = '<input type="text" style="display:none"' +
       var paramTemplate = '<input type="text" style="display:none"' +
@@ -69,12 +69,12 @@ function (angular, _, $) {
 
 
           function inputBlur(paramIndex) {
           function inputBlur(paramIndex) {
             /*jshint validthis:true */
             /*jshint validthis:true */
-
             var $input = $(this);
             var $input = $(this);
             var $link = $input.prev();
             var $link = $input.prev();
+            var newValue = $input.val();
 
 
-            if ($input.val() !== '' || func.def.params[paramIndex].optional) {
-              $link.text($input.val());
+            if (newValue !== '' || func.def.params[paramIndex].optional) {
+              $link.html(templateSrv.highlightVariablesAsHtml(newValue));
 
 
               func.updateParam($input.val(), paramIndex);
               func.updateParam($input.val(), paramIndex);
               scheduledRelinkIfNeeded();
               scheduledRelinkIfNeeded();
@@ -88,7 +88,6 @@ function (angular, _, $) {
 
 
           function inputKeyPress(paramIndex, e) {
           function inputKeyPress(paramIndex, e) {
             /*jshint validthis:true */
             /*jshint validthis:true */
-
             if(e.which === 13) {
             if(e.which === 13) {
               inputBlur.call(this, paramIndex);
               inputBlur.call(this, paramIndex);
             }
             }
@@ -147,7 +146,7 @@ function (angular, _, $) {
             $funcLink.appendTo(elem);
             $funcLink.appendTo(elem);
 
 
             _.each(funcDef.params, function(param, index) {
             _.each(funcDef.params, function(param, index) {
-              if (param.optional && !func.params[index]) {
+              if (param.optional && func.params.length <= index) {
                 return;
                 return;
               }
               }
 
 
@@ -155,7 +154,8 @@ function (angular, _, $) {
                 $('<span>, </span>').appendTo(elem);
                 $('<span>, </span>').appendTo(elem);
               }
               }
 
 
-              var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + func.params[index] + '</a>');
+              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
+              var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
               var $input = $(paramTemplate);
               var $input = $(paramTemplate);
 
 
               paramCountAtLink++;
               paramCountAtLink++;
@@ -239,4 +239,4 @@ function (angular, _, $) {
 
 
     });
     });
 
 
-});
+});

+ 134 - 0
src/app/directives/graphiteSegment.js

@@ -0,0 +1,134 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('graphiteSegment', function($compile, $sce) {
+      var inputTemplate = '<input type="text" data-provide="typeahead" ' +
+                            ' class="grafana-target-text-input input-medium"' +
+                            ' spellcheck="false" style="display:none"></input>';
+
+      var buttonTemplate = '<a class="grafana-target-segment" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
+
+      return {
+        link: function($scope, elem) {
+          var $input = $(inputTemplate);
+          var $button = $(buttonTemplate);
+          var segment = $scope.segment;
+          var options = null;
+          var cancelBlur = null;
+
+          $input.appendTo(elem);
+          $button.appendTo(elem);
+
+          $scope.updateVariableValue = function(value) {
+            if (value === '' || segment.value === value) {
+              return;
+            }
+
+            $scope.$apply(function() {
+              var selected = _.findWhere($scope.altSegments, { value: value });
+              if (selected) {
+                segment.value = selected.value;
+                segment.html = selected.html;
+                segment.expandable = selected.expandable;
+              }
+              else {
+                segment.value = value;
+                segment.html = $sce.trustAsHtml(value);
+                segment.expandable = true;
+              }
+              $scope.segmentValueChanged(segment, $scope.$index);
+            });
+          };
+
+          $scope.switchToLink = function(now) {
+            if (now === true || cancelBlur) {
+              clearTimeout(cancelBlur);
+              cancelBlur = null;
+              $input.hide();
+              $button.show();
+              $scope.updateVariableValue($input.val());
+            }
+            else {
+              // need to have long delay because the blur
+              // happens long before the click event on the typeahead options
+              cancelBlur = setTimeout($scope.switchToLink, 350);
+            }
+          };
+
+          $scope.source = function(query, callback) {
+            if (options) { return options; }
+
+            $scope.$apply(function() {
+              $scope.getAltSegments($scope.$index).then(function() {
+                options = _.map($scope.altSegments, function(alt) { return alt.value; });
+
+                // add custom values
+                if (segment.value !== 'select metric' &&  _.indexOf(options, segment.value) === -1) {
+                  options.unshift(segment.value);
+                }
+
+                callback(options);
+              });
+            });
+          };
+
+          $scope.updater = function(value) {
+            if (value === segment.value) {
+              clearTimeout(cancelBlur);
+              $input.focus();
+              return value;
+            }
+
+            $input.val(value);
+            $scope.switchToLink(true);
+
+            return value;
+          };
+
+          $input.attr('data-provide', 'typeahead');
+          $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater });
+
+          var typeahead = $input.data('typeahead');
+          typeahead.lookup = function () {
+            this.query = this.$element.val() || '';
+            var items = this.source(this.query, $.proxy(this.process, this));
+            return items ? this.process(items) : items;
+          };
+
+          $button.keydown(function(evt) {
+            // trigger typeahead on down arrow or enter key
+            if (evt.keyCode === 40 || evt.keyCode === 13) {
+              $button.click();
+            }
+          });
+
+          $button.click(function() {
+            options = null;
+            $input.css('width', ($button.width() + 16) + 'px');
+
+            $button.hide();
+            $input.show();
+            $input.focus();
+
+            var typeahead = $input.data('typeahead');
+            if (typeahead) {
+              $input.val('');
+              typeahead.lookup();
+            }
+          });
+
+          $input.blur($scope.switchToLink);
+
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});

+ 82 - 0
src/app/directives/templateParamSelector.js

@@ -0,0 +1,82 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('templateParamSelector', function($compile) {
+      var inputTemplate = '<input type="text" data-provide="typeahead" ' +
+                            ' class="grafana-target-text-input input-medium"' +
+                            ' spellcheck="false" style="display:none"></input>';
+
+      var buttonTemplate = '<a  class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>';
+
+      return {
+        link: function($scope, elem) {
+          var $input = $(inputTemplate);
+          var $button = $(buttonTemplate);
+          var variable = $scope.variable;
+
+          $input.appendTo(elem);
+          $button.appendTo(elem);
+
+          function updateVariableValue(value) {
+            $scope.$apply(function() {
+              var selected = _.findWhere(variable.options, { text: value });
+              if (!selected) {
+                selected = { text: value, value: value };
+              }
+              $scope.setVariableValue($scope.variable, selected);
+            });
+          }
+
+          $input.attr('data-provide', 'typeahead');
+          $input.typeahead({
+            minLength: 0,
+            items: 1000,
+            updater: function(value) {
+              $input.val(value);
+              $input.trigger('blur');
+              return value;
+            }
+          });
+
+          var typeahead = $input.data('typeahead');
+          typeahead.lookup = function () {
+            var options = _.map(variable.options, function(option) { return option.text; });
+            this.query = this.$element.val() || '';
+            return this.process(options);
+          };
+
+          $button.click(function() {
+            $input.css('width', ($button.width() + 16) + 'px');
+
+            $button.hide();
+            $input.show();
+            $input.focus();
+
+            var typeahead = $input.data('typeahead');
+            if (typeahead) {
+              $input.val('');
+              typeahead.lookup();
+            }
+
+          });
+
+          $input.blur(function() {
+            if ($input.val() !== '') { updateVariableValue($input.val()); }
+            $input.hide();
+            $button.show();
+            $button.focus();
+          });
+
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});

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

+ 3 - 56
src/app/filters/all.js

@@ -9,18 +9,6 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
     };
     };
   });
   });
 
 
-  /*
-    Filter an array of objects by elasticsearch version requirements
-  */
-  module.filter('esVersion', function(esVersion) {
-    return function(items, require) {
-      var ret = _.filter(items,function(qt) {
-        return esVersion.is(qt[require]) ? true : false;
-      });
-      return ret;
-    };
-  });
-
   module.filter('slice', function() {
   module.filter('slice', function() {
     return function(arr, start, end) {
     return function(arr, start, end) {
       if(!_.isUndefined(arr)) {
       if(!_.isUndefined(arr)) {
@@ -67,51 +55,10 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
     };
     };
   });
   });
 
 
-  module.filter('urlLink', function() {
-    var  //URLs starting with http://, https://, or ftp://
-      r1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim,
-      //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
-      r2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim,
-      //Change email addresses to mailto:: links.
-      r3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim;
-
-    var urlLink = function(text) {
-      var t1,t2,t3;
-      if(!_.isString(text)) {
-        return text;
-      } else {
-        _.each(text.match(r1), function() {
-          t1 = text.replace(r1, "<a href=\"$1\" target=\"_blank\">$1</a>");
-        });
-        text = t1 || text;
-        _.each(text.match(r2), function() {
-          t2 = text.replace(r2, "$1<a href=\"http://$2\" target=\"_blank\">$2</a>");
-        });
-        text = t2 || text;
-        _.each(text.match(r3), function() {
-          t3 = text.replace(r3, "<a href=\"mailto:$1\">$1</a>");
-        });
-        text = t3 || text;
-        return text;
-      }
-    };
+  module.filter('interpolateTemplateVars', function(templateSrv) {
     return function(text) {
     return function(text) {
-      return _.isArray(text)
-        ? _.map(text, urlLink)
-        : urlLink(text);
-    };
-  });
-
-  module.filter('gistid', function() {
-    var gist_pattern = /(\d{5,})|([a-z0-9]{10,})|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
-    return function(input) {
-      if(!(_.isUndefined(input))) {
-        var output = input.match(gist_pattern);
-        if(!_.isNull(output) && !_.isUndefined(output)) {
-          return output[0].replace(/.*\//, '');
-        }
-      }
+      return templateSrv.replaceWithText(text);
     };
     };
   });
   });
 
 
-});
+});

+ 0 - 67
src/app/panels/annotations/editor.html

@@ -1,67 +0,0 @@
-<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
-	<div class="modal-body">
-  <div class="pull-right editor-title">Annotations</div>
-
-  <div class="editor-row">
-    <table class="table table-striped annotation-editor-table" style="width: 700px">
-      <thead>
-        <th width="90%">Name</th>
-        <th width="1%"></th>
-        <th width="1%"></th>
-        <th width="1%"></th>
-      </thead>
-      <tr ng-repeat="annotation in panel.annotations">
-        <td>
-          <a ng-click="edit(annotation)" bs-tooltip="'Click to edit'">
-            <i class="icon-cog"></i>
-            {{annotation.name}}
-          </a>
-        </td>
-        <td><i ng-click="_.move(panel.annotations,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
-        <td><i ng-click="_.move(panel.annotations,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
-        <td><i ng-click="panel.annotations = _.without(panel.annotations, annotation)" class="pointer icon-remove"></i></td>
-      </tr>
-    </table>
-  </div>
-
-  <div class="editor-row">
-    <h4 ng-show="currentIsNew">Add Annotation</h4>
-    <h4 ng-show="!currentIsNew">Edit Annotation</h4>
-
-    <div class="editor-option">
-      <label class="small">Name</label>
-      <input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
-    </div>
-    <div class="editor-option">
-      <label class="small">Datasource</label>
-      <select ng-model="currentDatasource" ng-options="f.name for f in datasources" ng-change="setDatasource()"></select>
-    </div>
-    <div class="editor-option">
-      <label class="small">Icon color</label>
-       <spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
-    </div>
-     <div class="editor-option">
-      <label class="small">Icon size</label>
-      <select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
-    </div>
-    <div class="editor-option">
-      <label class="small">Grid line</label>
-      <input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
-    </div>
-    <div class="editor-option">
-      <label class="small">Line color</label>
-       <spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
-    </div>
-  </div>
-
-  <div ng-include src="currentDatasource.editorSrc">
-  </div>
-
-</div>
-
-<div class="modal-footer">
-  <button ng-show="currentIsNew" type="button" class="btn btn-success" ng-click="add()">Add annotation</button>
-  <button ng-show="!currentIsNew" type="button" class="btn btn-success" ng-click="update()">Update</button>
-  <button type="button" class="btn btn-danger" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
-</div>
-</div>

+ 0 - 12
src/app/panels/annotations/module.html

@@ -1,12 +0,0 @@
-<div ng-controller='AnnotationsCtrl' ng-init="init()">
-
-  <div class="submenu-toggle" ng-repeat="annotation in panel.annotations" ng-class="{'annotation-disabled': !annotation.enable }">
-    <i class="annotation-color-icon icon-minus"></i>
-    <a ng-click="hide(annotation)" class="small">{{annotation.name}}</a>
-  </div>
-
-  <div class="submenu-control-edit">
-    <i class="icon-cog pointer" config-modal="app/panels/annotations/editor.html" bs-tooltip="'Edit annotations'" ></i>
-  </div>
-
-</div>

+ 0 - 39
src/app/panels/annotations/module.js

@@ -1,39 +0,0 @@
-/*
-
-  ## annotations
-
-*/
-define([
-  'angular',
-  'app',
-  'lodash',
-  './editor'
-],
-function (angular, app, _) {
-  'use strict';
-
-  var module = angular.module('grafana.panels.annotations', []);
-  app.useModule(module);
-
-  module.controller('AnnotationsCtrl', function($scope, datasourceSrv, $rootScope) {
-
-    $scope.panelMeta = {
-      status  : "Stable",
-      description : "Annotations"
-    };
-
-    // Set and populate defaults
-    var _d = {
-      annotations: []
-    };
-
-    _.defaults($scope.panel, _d);
-
-    $scope.hide = function (annotation) {
-      annotation.enable = !annotation.enable;
-      $rootScope.$broadcast('refresh');
-    };
-
-  });
-
-});

+ 0 - 50
src/app/panels/filtering/module.html

@@ -1,50 +0,0 @@
-<div ng-controller='filtering' ng-init="init()">
-
-  <div class='filtering-container'>
-
-    <div ng-repeat="filter in filter.templateParameters" class="small filter-panel-filter">
-      <div>
-        <i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(filter)"></i>
-        <i class="filter-action pointer icon-edit" ng-hide="filter.editing" bs-tooltip="'Edit'" ng-click="filter.editing = true"></i>
-      </div>
-
-      <div ng-hide="filter.editing" style="margin-right: 45px;">
-        <ul class="unstyled">
-          <li ng-if="filter.name" class="dropdown">
-            {{filter.name}} :
-            <a class="dropdown-toggle" data-toggle="dropdown">
-              {{filter.current.text}}
-            </a>
-              <ul class="dropdown-menu">
-                <li ng-repeat="option in filter.options">
-                  <a ng-click="filterOptionSelected(filter, option)">{{option.text}}</a>
-                </li>
-              </ul>
-          </li>
-        </ul>
-      </div>
-
-      <form ng-show="filter.editing">
-        <ul class="unstyled">
-          <li>
-            <strong>name</strong>:<br/>
-            <input type='text' ng-model="filter.name">
-          </li>
-          <li>
-            <strong>filter.query</strong>:<br/>
-            <input type='text' ng-model="filter.query">
-          </li>
-          <li>
-            <label for="includeAll">Include all:</label>
-            <input id="includeAll" type='checkbox' ng-model="filter.includeAll">
-          </li>
-        </ul>
-        <div>
-          <input type="submit" value="Update" ng-click="applyFilter(filter)" class="filter-apply btn btn-success btn-mini" bs-tooltip="'Update and refresh'"/>
-          <button ng-click="filter.editing=undefined" class="filter-apply btn btn-mini" bs-tooltip="'Save without refresh'">Close</button>
-        </div>
-      </form>
-    </div>
-    <i class="pointer icon-plus-sign add-filter-action" ng-click="add()" bs-tooltip="'Add metric filter / param'" data-placement="right"></i>
-  </div>
-</div>

+ 0 - 104
src/app/panels/filtering/module.js

@@ -1,104 +0,0 @@
-/*
-
-  ## filtering
-
-*/
-define([
-  'angular',
-  'app',
-  'lodash'
-],
-function (angular, app, _) {
-  'use strict';
-
-  var module = angular.module('grafana.panels.filtering', []);
-  app.useModule(module);
-
-  module.controller('filtering', function($scope, datasourceSrv, $rootScope, $timeout, $q) {
-
-    $scope.panelMeta = {
-      status  : "Stable",
-      description : "graphite target filters"
-    };
-
-    // Set and populate defaults
-    var _d = {
-    };
-    _.defaults($scope.panel,_d);
-
-    $scope.init = function() {
-      // empty. Don't know if I need the function then.
-    };
-
-    $scope.remove = function(templateParameter) {
-      $scope.filter.removeTemplateParameter(templateParameter);
-    };
-
-    $scope.filterOptionSelected = function(templateParameter, option, recursive) {
-      templateParameter.current = option;
-
-      $scope.filter.updateTemplateData();
-
-      return $scope.applyFilterToOtherFilters(templateParameter)
-        .then(function() {
-          // only refresh in the outermost call
-          if (!recursive) {
-            $scope.dashboard.emit_refresh();
-          }
-        });
-    };
-
-    $scope.applyFilterToOtherFilters = function(updatedTemplatedParam) {
-      var promises = _.map($scope.filter.templateParameters, function(templateParam) {
-        if (templateParam === updatedTemplatedParam) {
-          return;
-        }
-        if (templateParam.query.indexOf('[[' + updatedTemplatedParam.name + ']]') !== -1) {
-          return $scope.applyFilter(templateParam);
-        }
-      });
-
-      return $q.all(promises);
-    };
-
-    $scope.applyFilter = function(templateParam) {
-      return datasourceSrv.default.metricFindQuery($scope.filter, templateParam.query)
-        .then(function (results) {
-          templateParam.editing = undefined;
-          templateParam.options = _.map(results, function(node) {
-            return { text: node.text, value: node.text };
-          });
-
-          if (templateParam.includeAll) {
-            var allExpr = '{';
-            _.each(templateParam.options, function(option) {
-              allExpr += option.text + ',';
-            });
-            allExpr = allExpr.substring(0, allExpr.length - 1) + '}';
-            templateParam.options.unshift({text: 'All', value: allExpr});
-          }
-
-          // if parameter has current value
-          // if it exists in options array keep value
-          if (templateParam.current) {
-            var currentExists = _.findWhere(templateParam.options, { value: templateParam.current.value });
-            if (currentExists) {
-              return $scope.filterOptionSelected(templateParam, templateParam.current, true);
-            }
-          }
-
-          return $scope.filterOptionSelected(templateParam, templateParam.options[0], true);
-        });
-    };
-
-    $scope.add = function() {
-      $scope.filter.addTemplateParameter({
-        type      : 'filter',
-        name      : 'filter name',
-        editing   : true,
-        query     : 'metric.path.query.*',
-      });
-    };
-
-  });
-});

+ 19 - 10
src/app/panels/graph/module.html

@@ -21,14 +21,23 @@
 
 
   <div class="clearfix"></div>
   <div class="clearfix"></div>
 
 
-  <div class="panel-full-edit-tabs" ng-if="editMode">
-    <div ng-model="editor.index" bs-tabs>
-      <div ng-repeat="tab in editorTabs" data-title="{{tab}}">
-      </div>
-    </div>
-
-    <div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
-      <div ng-include src="tab.src"></div>
-    </div>
-  </div>
+	<div style="margin-top: 30px" ng-if="editMode">
+		<div class="dashboard-editor-header">
+			<div class="dashboard-editor-title">
+				<i class="icon icon-bar-chart"></i>
+				Graph
+			</div>
+
+			<div ng-model="editor.index" bs-tabs>
+				<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
+				</div>
+			</div>
+		</div>
+
+		<div class="dashboard-editor-body">
+			<div ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
+				<div ng-include src="tab.src"></div>
+			</div>
+		</div>
+	</div>
 </div>
 </div>

+ 15 - 13
src/app/panels/graph/module.js

@@ -23,7 +23,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
   var module = angular.module('grafana.panels.graph');
   var module = angular.module('grafana.panels.graph');
   app.useModule(module);
   app.useModule(module);
 
 
-  module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) {
+  module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) {
 
 
     $scope.panelMeta = {
     $scope.panelMeta = {
       modals : [],
       modals : [],
@@ -179,16 +179,10 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
     $scope.hiddenSeries = {};
     $scope.hiddenSeries = {};
 
 
     $scope.updateTimeRange = function () {
     $scope.updateTimeRange = function () {
-      $scope.range = $scope.filter.timeRange();
-      $scope.rangeUnparsed = $scope.filter.timeRange(false);
+      $scope.range = timeSrv.timeRange();
+      $scope.rangeUnparsed = timeSrv.timeRange(false);
       $scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
       $scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
-      $scope.interval = '10m';
-
-      if ($scope.range) {
-        $scope.interval = kbn.secondsToHms(
-          kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.resolution, 0) / 1000
-        );
-      }
+      $scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
     };
     };
 
 
     $scope.get_data = function() {
     $scope.get_data = function() {
@@ -203,13 +197,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
         cacheTimeout: $scope.panel.cacheTimeout
         cacheTimeout: $scope.panel.cacheTimeout
       };
       };
 
 
-      $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.filter, $scope.rangeUnparsed, $scope.dashboard);
+      $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard);
 
 
-      return $scope.datasource.query($scope.filter, metricsQuery)
+      return $scope.datasource.query(metricsQuery)
         .then($scope.dataHandler)
         .then($scope.dataHandler)
         .then(null, function(err) {
         .then(null, function(err) {
           $scope.panelMeta.loading = false;
           $scope.panelMeta.loading = false;
-          $scope.panel.error = err.message || "Timeseries data request error";
+          $scope.panelMeta.error = err.message || "Timeseries data request error";
           $scope.inspector.error = err;
           $scope.inspector.error = err;
           $scope.render([]);
           $scope.render([]);
         });
         });
@@ -355,6 +349,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       $scope.render();
       $scope.render();
     };
     };
 
 
+    $scope.toggleEditorHelp = function(index) {
+      if ($scope.editorHelpIndex === index) {
+        $scope.editorHelpIndex = null;
+        return;
+      }
+      $scope.editorHelpIndex = index;
+    };
+
     panelSrv.init($scope);
     panelSrv.init($scope);
   });
   });
 
 

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

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

+ 1 - 1
src/app/panels/text/module.html

@@ -1,4 +1,4 @@
 <div ng-controller='text'>
 <div ng-controller='text'>
-  <p ng-bind-html="content">
+  <p ng-bind-html="content" ng-show="content">
   </p>
   </p>
 </div>
 </div>

+ 2 - 3
src/app/panels/text/module.js

@@ -3,7 +3,6 @@ define([
   'app',
   'app',
   'lodash',
   'lodash',
   'require',
   'require',
-  'services/filterSrv'
 ],
 ],
 function (angular, app, _, require) {
 function (angular, app, _, require) {
   'use strict';
   'use strict';
@@ -13,7 +12,7 @@ function (angular, app, _, require) {
 
 
   var converter;
   var converter;
 
 
-  module.controller('text', function($scope, filterSrv, $sce, panelSrv) {
+  module.controller('text', function($scope, templateSrv, $sce, panelSrv) {
 
 
     $scope.panelMeta = {
     $scope.panelMeta = {
       description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
       description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
@@ -76,7 +75,7 @@ function (angular, app, _, require) {
 
 
     $scope.updateContent = function(html) {
     $scope.updateContent = function(html) {
       try {
       try {
-        $scope.content = $sce.trustAsHtml(filterSrv.applyTemplateToTarget(html));
+        $scope.content = $sce.trustAsHtml(templateSrv.replace(html));
       } catch(e) {
       } catch(e) {
         console.log('Text panel error: ', e);
         console.log('Text panel error: ', e);
         $scope.content = $sce.trustAsHtml(html);
         $scope.content = $sce.trustAsHtml(html);

+ 73 - 67
src/app/panels/timepicker/custom.html

@@ -1,78 +1,84 @@
-  <div class="modal-body">
-    <style>
-      .timepicker-to-column {
-        margin-top: 10px;
-      }
+<div class="dashboard-editor-header">
+	<div class="dashboard-editor-title">
+		<i class="icon icon-calendar-empty"></i>
+		Custom time range
+	</div>
+</div>
 
 
-      .timepicker-input input {
-        outline: 0 !important;
-        border: 0px !important;
-        -webkit-box-shadow: 0;
-        -moz-box-shadow: 0;
-        box-shadow: 0;
-        position: relative;
-      }
+<div class="dashboard-editor-body">
+	<style>
+		.timepicker-to-column {
+			margin-top: 10px;
+		}
 
 
-      .timepicker-input input::-webkit-outer-spin-button,
-      .timepicker-input input::-webkit-inner-spin-button {
-          -webkit-appearance: none;
-          margin: 0;
-      }
+		.timepicker-input input {
+			outline: 0 !important;
+			border: 0px !important;
+			-webkit-box-shadow: 0;
+			-moz-box-shadow: 0;
+			box-shadow: 0;
+			position: relative;
+		}
 
 
-      input.timepicker-date {
-        width: 90px;
-      }
-      input.timepicker-hms {
-        width: 20px;
-      }
-      input.timepicker-ms {
-        width: 25px;
-      }
-      div.timepicker-now {
-        float: right;
-      }
-    </style>
+		.timepicker-input input::-webkit-outer-spin-button,
+		.timepicker-input input::-webkit-inner-spin-button {
+			-webkit-appearance: none;
+			margin: 0;
+		}
 
 
-    <div class="timepicker form-horizontal">
-        <form name="input">
+		input.timepicker-date {
+			width: 90px;
+		}
+		input.timepicker-hms {
+			width: 20px;
+		}
+		input.timepicker-ms {
+			width: 25px;
+		}
+		div.timepicker-now {
+			float: right;
+		}
+	</style>
 
 
-        <div class="timepicker-from-column">
-          <label class="small">From</label>
-          <div class="fake-input timepicker-input">
-            <input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
-            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
-            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
-            <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
-            <input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond"  onClick="this.select();"/>
-          </div>
-        </div>
+	<div class="timepicker form-horizontal">
+		<form name="input">
 
 
-        <div class="timepicker-to-column">
+			<div class="timepicker-from-column">
+				<label class="small">From</label>
+				<div class="fake-input timepicker-input">
+					<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
+					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
+					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
+					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
+					<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond"  onClick="this.select();"/>
+				</div>
+			</div>
 
 
-          <label class="small">To (<a class="link" ng-class="{'strong':tempnow}" ng-click="setNow();tempnow=true">now</a>)</label>
+			<div class="timepicker-to-column">
 
 
-          <div class="fake-input timepicker-input">
-            <div ng-hide="tempnow">
-              <input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
-              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
-              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
-              <input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
-              <input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
-            </div>
-            <span type="text" ng-show="tempnow" ng-disabled="tempnow">&nbsp <i class="pointer icon-remove-sign" ng-click="setNow();tempnow=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
-          </div>
-        </div>
+				<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="setNow();temptime.now=true">now</a>)</label>
 
 
-        </form>
-        <div class="clearfix"></div>
-    </div>
-  </div>
+				<div class="fake-input timepicker-input">
+					<div ng-hide="temptime.now">
+						<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
+						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
+						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
+						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
+						<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
+					</div>
+					<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">&nbsp <i class="pointer icon-remove-sign" ng-click="setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
+				</div>
+			</div>
 
 
-  <div class="modal-footer">
-    <form name="input" style="margin-bottom:0">
-    <span class="" ng-hide="input.$valid">Invalid date or range</span>
-    <button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
-    <button ng-click="dismiss();" class="btn btn-danger">Cancel</button>
+		</form>
+		<div class="clearfix"></div>
+	</div>
+</div>
 
 
-    </form>
-  </div>
+<div class="dashboard-editor-footer">
+	<form name="input" style="margin-bottom:0">
+		<span class="" ng-hide="input.$valid">Invalid date or range</span>
+		<button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
+		<button ng-click="dismiss();" class="btn btn-success pull-right">Cancel</button>
+	</form>
+</div>

+ 4 - 5
src/app/panels/timepicker/module.html

@@ -16,8 +16,7 @@
       <li class="dropdown">
       <li class="dropdown">
 
 
         <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
         <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
-          <span ng-show="filter.time" ng-bind="time.rangeString"></span>
-          <span ng-hide="filter.time">Time filter</span>
+          <span ng-bind="time.rangeString"></span>
           <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
           <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
           <i class="icon-caret-down"></i>
           <i class="icon-caret-down"></i>
         </a>
         </a>
@@ -33,10 +32,10 @@
             <a href="#">Auto-Refresh</a>
             <a href="#">Auto-Refresh</a>
             <ul class="dropdown-menu">
             <ul class="dropdown-menu">
               <li>
               <li>
-                <a ng-click="dashboard.set_interval(false)">Off</a>
+                <a ng-click="timeSrv.set_interval(false)">Off</a>
               </li>
               </li>
               <li bindonce ng-repeat="interval in panel.refresh_intervals track by $index">
               <li bindonce ng-repeat="interval in panel.refresh_intervals track by $index">
-                <a ng-click="dashboard.set_interval(interval)" bo-text="'Every ' + interval"></a>
+                <a ng-click="timeSrv.set_interval(interval)" bo-text="'Every ' + interval"></a>
               </li>
               </li>
             </ul>
             </ul>
           </li>
           </li>
@@ -45,7 +44,7 @@
 
 
       </li>
       </li>
       <li ng-show="!dashboard.refresh" class="grafana-menu-refresh">
       <li ng-show="!dashboard.refresh" class="grafana-menu-refresh">
-        <a ng-click="dashboard.emit_refresh()"><i class="icon-refresh"></i></a>
+        <a ng-click="timeSrv.refreshDashboard()"><i class="icon-refresh"></i></a>
       </li>
       </li>
     </ul>
     </ul>
 
 

+ 14 - 26
src/app/panels/timepicker/module.js

@@ -25,11 +25,11 @@ function (angular, app, _, moment, kbn) {
   var module = angular.module('grafana.panels.timepicker', []);
   var module = angular.module('grafana.panels.timepicker', []);
   app.useModule(module);
   app.useModule(module);
 
 
-  module.controller('timepicker', function($scope, $modal, $q) {
+  module.controller('timepicker', function($scope, $rootScope, timeSrv) {
+
     $scope.panelMeta = {
     $scope.panelMeta = {
       status  : "Stable",
       status  : "Stable",
-      description : "A panel for controlling the time range filters. If you have time based data, "+
-        " or if you're using time stamped indices, you need one of these"
+      description : ""
     };
     };
 
 
     // Set and populate defaults
     // Set and populate defaults
@@ -39,8 +39,6 @@ function (angular, app, _, moment, kbn) {
       refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
       refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
     };
     };
 
 
-    var customTimeModal = null;
-
     _.defaults($scope.panel,_d);
     _.defaults($scope.panel,_d);
 
 
     // ng-pattern regexs
     // ng-pattern regexs
@@ -52,33 +50,25 @@ function (angular, app, _, moment, kbn) {
       millisecond: /^[0-9]*$/
       millisecond: /^[0-9]*$/
     };
     };
 
 
+    $scope.timeSrv = timeSrv;
+
     $scope.$on('refresh', function() {
     $scope.$on('refresh', function() {
       $scope.init();
       $scope.init();
     });
     });
 
 
     $scope.init = function() {
     $scope.init = function() {
-      var time = this.filter.timeRange(true);
+      var time = timeSrv.timeRange(true);
       if(time) {
       if(time) {
-        $scope.panel.now = this.filter.timeRange(false).to === "now" ? true : false;
+        $scope.panel.now = timeSrv.timeRange(false).to === "now" ? true : false;
         $scope.time = getScopeTimeObj(time.from,time.to);
         $scope.time = getScopeTimeObj(time.from,time.to);
       }
       }
     };
     };
 
 
     $scope.customTime = function() {
     $scope.customTime = function() {
-      if (!customTimeModal) {
-        customTimeModal = $modal({
-          template: './app/panels/timepicker/custom.html',
-          persist: true,
-          show: false,
-          scope: $scope,
-          keyboard: false
-        });
-      }
-
       // Assume the form is valid since we're setting it to something valid
       // Assume the form is valid since we're setting it to something valid
       $scope.input.$setValidity("dummy", true);
       $scope.input.$setValidity("dummy", true);
       $scope.temptime = cloneTime($scope.time);
       $scope.temptime = cloneTime($scope.time);
-      $scope.tempnow = $scope.panel.now;
+      $scope.temptime.now = $scope.panel.now;
 
 
       $scope.temptime.from.date.setHours(0,0,0,0);
       $scope.temptime.from.date.setHours(0,0,0,0);
       $scope.temptime.to.date.setHours(0,0,0,0);
       $scope.temptime.to.date.setHours(0,0,0,0);
@@ -89,9 +79,7 @@ function (angular, app, _, moment, kbn) {
         $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
         $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
       }
       }
 
 
-      $q.when(customTimeModal).then(function(modalEl) {
-        modalEl.modal('show');
-      });
+      $scope.emitAppEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });
     };
     };
 
 
     // Constantly validate the input of the fields. This function does not change any date variables
     // Constantly validate the input of the fields. This function does not change any date variables
@@ -118,7 +106,7 @@ function (angular, app, _, moment, kbn) {
         return false;
         return false;
       }
       }
 
 
-      return {from:_from,to:_to};
+      return { from: _from, to:_to, now: time.now};
     };
     };
 
 
     $scope.setNow = function() {
     $scope.setNow = function() {
@@ -135,12 +123,12 @@ function (angular, app, _, moment, kbn) {
       // Create filter object
       // Create filter object
       var _filter = _.clone(time);
       var _filter = _.clone(time);
 
 
-      if($scope.tempnow) {
+      if(time.now) {
         _filter.to = "now";
         _filter.to = "now";
       }
       }
 
 
       // Set the filter
       // Set the filter
-      $scope.panel.filter_id = $scope.filter.setTime(_filter);
+      $scope.panel.filter_id = timeSrv.setTime(_filter);
 
 
       // Update our representation
       // Update our representation
       $scope.time = getScopeTimeObj(time.from,time.to);
       $scope.time = getScopeTimeObj(time.from,time.to);
@@ -154,7 +142,7 @@ function (angular, app, _, moment, kbn) {
         to: "now"
         to: "now"
       };
       };
 
 
-      this.filter.setTime(_filter);
+      timeSrv.setTime(_filter);
 
 
       $scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
       $scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
     };
     };
@@ -187,7 +175,7 @@ function (angular, app, _, moment, kbn) {
         model.tooltip = 'Click to set time filter';
         model.tooltip = 'Click to set time filter';
       }
       }
 
 
-      if ($scope.filter.time) {
+      if (timeSrv.time) {
         if ($scope.panel.now) {
         if ($scope.panel.now) {
           model.rangeString = moment(model.from.date).fromNow() + ' to ' +
           model.rangeString = moment(model.from.date).fromNow() + ' to ' +
             moment(model.to.date).fromNow();
             moment(model.to.date).fromNow();

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

@@ -0,0 +1,85 @@
+<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
+
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-bolt"></i>
+			Annotations
+		</div>
+
+		<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+			<div ng-repeat="tab in ['Overview', 'Add', 'Edit']" data-title="{{tab}}">
+			</div>
+		</div>
+
+	</div>
+
+	<div class="dashboard-editor-body">
+		<div class="editor-row row" ng-if="editor.index == 0">
+			<div class="span6">
+				<div ng-if="variables.length === 0">
+					<em>No annotations defined</em>
+				</div>
+				<table class="grafana-options-table">
+					<tr ng-repeat="annotation in annotations">
+						<td style="width:90%">
+							<i class="icon-bolt"></i> &nbsp;
+							{{annotation.name}}
+						</td>
+						<td style="width: 1%">
+							<a ng-click="edit(annotation)" class="btn btn-success btn-mini">
+								<i class="icon-edit"></i>
+								Edit
+							</a>
+						</td>
+						<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+						<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+						<td style="width: 1%">
+							<a ng-click="removeAnnotation(annotation)" class="btn btn-danger btn-mini">
+								<i class="icon-remove"></i>
+							</a>
+						</td>
+					</tr>
+				</table>
+			</div>
+		</div>
+
+		<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
+			<div class="editor-row">
+				<div class="editor-option">
+					<label class="small">Name</label>
+					<input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
+				</div>
+				<div class="editor-option">
+					<label class="small">Datasource</label>
+					<select ng-model="currentAnnotation.datasource" ng-options="f.name as f.name for f in datasources" ng-change="datasourceChanged()"></select>
+				</div>
+				<div class="editor-option">
+					<label class="small">Icon color</label>
+					<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
+				</div>
+				<div class="editor-option">
+					<label class="small">Icon size</label>
+					<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
+				</div>
+				<div class="editor-option">
+					<label class="small">Grid line</label>
+					<input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
+				</div>
+				<div class="editor-option">
+					<label class="small">Line color</label>
+					<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
+				</div>
+			</div>
+
+			<div ng-include src="currentDatasource.editorSrc">
+			</div>
+
+		</div>
+	</div>
+
+	<div class="dashboard-editor-footer">
+		<button ng-show="editor.index === 1" type="button" class="btn btn-success" ng-click="add()">Add</button>
+		<button ng-show="editor.index === 2" type="button" class="btn btn-success pull-left" ng-click="update();">Update</button>
+		<button type="button" class="btn btn-success pull-right" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
+	</div>
+</div>

+ 97 - 111
src/app/partials/dashboard.html

@@ -1,127 +1,113 @@
-<div ng-controller="DashboardCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
+<div ng-controller="DashboardCtrl" body-class class="dashboard" ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
 
 
 	<div ng-include="'app/partials/pro/dashboard_topnav.html'">
 	<div ng-include="'app/partials/pro/dashboard_topnav.html'">
 	</div>
 	</div>
 
 
-	<div class="submenu-controls">
-    <div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
-      <div class="submenu-panel-title">
-        <span class="small"><strong>{{pulldown.type}}:</strong></span>
-      </div>
-      <div class="submenu-panel-wrapper">
-        <grafana-simple-panel type="pulldown.type" ng-cloak></grafana-simple-panel>
-      </div>
-    </div>
-    <div class="clearfix"></div>
+	<div ng-if="submenuEnabled" ng-include="'app/partials/submenu.html'">
   </div>
   </div>
 
 
   <div class="clearfix"></div>
   <div class="clearfix"></div>
-  <div class="container-fluid main">
-    <div>
-      <div class="grafana-container container">
-        <!-- Rows -->
-        <div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
-          <div class="row-control">
-            <div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
-              <div class="row-close" ng-show="row.collapse" data-placement="bottom" >
-								<div class="row-close-buttons">
-									<span class="row-button bgWarning" config-modal="app/partials/roweditor.html" class="pointer">
-										<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
-									</span>
-									<span class="row-button bgPrimary" ng-click="toggle_row(row)">
-										<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
-									</span>
-								</div>
-                <span class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></span>
-              </div>
-              <div class="row-open" ng-show="!row.collapse">
-                <div class='row-tab bgPrimary' ng-click="toggle_row(row)">
-                  <span class="row-tab-button">
-                    <i class="icon-caret-right"></i>
-                  </span>
-                </div>
-                <div class='row-tab bgSuccess dropdown' ng-show="row.editable">
-                  <span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
-                    <i class="icon-th-list"></i>
-                  </span>
-                  <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
-                    <li class="dropdown-submenu">
-                    <a href="javascript:void(0);">Add Panel</a>
-                      <ul class="dropdown-menu">
-												<li bindonce ng-repeat="name in panelNames">
-													<a ng-click="add_panel_default(name)" bo-text="name"></a>
-												</li>
-                      </ul>
-                    </li>
-                    <li class="dropdown-submenu">
-                    <a href="javascript:void(0);">Set height</a>
-                      <ul class="dropdown-menu">
-                      <li><a ng-click="set_height('100px')">100 px</a></li>
-                      <li><a ng-click="set_height('150px')">150 px</a></li>
-                      <li><a ng-click="set_height('200px')">200 px</a></li>
-                      <li><a ng-click="set_height('250px')">250 px</a></li>
-                      <li><a ng-click="set_height('300px')">300 px</a></li>
-                      <li><a ng-click="set_height('350px')">350 px</a></li>
-                      <li><a ng-click="set_height('450px')">450 px</a></li>
-                      <li><a ng-click="set_height('500px')">500 px</a></li>
-                      <li><a ng-click="set_height('600px')">600 px</a></li>
-                      <li><a ng-click="set_height('700px')">700 px</a></li>
-                      </ul>
-                    </li>
-                    <li class="dropdown-submenu">
-                    <a href="javascript:void(0);">Move</a>
-                      <ul class="dropdown-menu">
-                      <li><a ng-click="move_row(-1)">Up</a></li>
-                      <li><a ng-click="move_row(1)">Down</a></li>
-                      </ul>
-                    </li>
-                    <li>
-                      <a config-modal="app/partials/roweditor.html">Row editor</a>
-                    </li>
-                    <li>
-                      <a ng-click="delete_row()">Delete row</a>
-                    </li>
-                  </ul>
-                </div>
-              </div>
-            </div>
 
 
-            <div style="padding-top:0px" ng-if="!row.collapse">
-							<div class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
-							</div>
+	<div dash-editor-view>
+	</div>
 
 
-              <!-- Panels -->
-							<div ng-repeat="(name, panel) in row.panels"
-									 class="panel nospace"
-									 style="position:relative"
-									 data-drop="true"
-									 panel-width
-									 ng-model="panel"
-									 data-jqyoui-options
-									 jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
-									 ng-class="{'dragInProgress':dashboard.$$panelDragging}">
-                <grafana-panel type="panel.type" ng-cloak></grafana-panel>
-              </div>
+	<div class="main-view-container">
+		<div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
+			<div class="row-control">
+				<div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
+					<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
+						<div class="row-close-buttons">
+							<span class="row-button bgPrimary" ng-click="toggle_row(row)">
+								<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
+							</span>
+						</div>
+						<span class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></span>
+					</div>
+					<div class="row-open" ng-show="!row.collapse">
+						<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
+							<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
+								<i class="icon-th-list"></i>
+							</span>
+							<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
+								<li>
+									<a ng-click="toggle_row(row)">Collapse row</a>
+								</li>
+								<li class="dropdown-submenu">
+									<a href="javascript:void(0);">Add Panel</a>
+									<ul class="dropdown-menu">
+										<li bindonce ng-repeat="name in panelNames">
+											<a ng-click="add_panel_default(name)" bo-text="name"></a>
+										</li>
+									</ul>
+								</li>
+								<li class="dropdown-submenu">
+									<a href="javascript:void(0);">Set height</a>
+									<ul class="dropdown-menu">
+										<li><a ng-click="set_height('25px')">25 px</a></li>
+										<li><a ng-click="set_height('100px')">100 px</a></li>
+										<li><a ng-click="set_height('150px')">150 px</a></li>
+										<li><a ng-click="set_height('200px')">200 px</a></li>
+										<li><a ng-click="set_height('250px')">250 px</a></li>
+										<li><a ng-click="set_height('300px')">300 px</a></li>
+										<li><a ng-click="set_height('350px')">350 px</a></li>
+										<li><a ng-click="set_height('450px')">450 px</a></li>
+										<li><a ng-click="set_height('500px')">500 px</a></li>
+										<li><a ng-click="set_height('600px')">600 px</a></li>
+										<li><a ng-click="set_height('700px')">700 px</a></li>
+									</ul>
+								</li>
+								<li class="dropdown-submenu">
+									<a href="javascript:void(0);">Move</a>
+									<ul class="dropdown-menu">
+										<li><a ng-click="move_row(-1)">Up</a></li>
+										<li><a ng-click="move_row(1)">Down</a></li>
+									</ul>
+								</li>
+								<li>
+									<a dash-editor-link="app/partials/roweditor.html">Row editor</a>
+								</li>
+								<li>
+									<a ng-click="delete_row()">Delete row</a>
+								</li>
+							</ul>
+						</div>
+					</div>
+				</div>
 
 
-              <div panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
-              </div>
+				<div style="padding-top:0px" ng-if="!row.collapse">
+					<div class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
+					</div>
 
 
-              <div class="clearfix"></div>
-              </div>
-            </div>
-          </div>
-        </div>
+					<!-- Panels -->
+					<div ng-repeat="(name, panel) in row.panels"
+						class="panel nospace"
+						style="position:relative"
+						data-drop="true"
+						panel-width
+						ng-model="panel"
+						data-jqyoui-options
+						jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
+						ng-class="{'dragInProgress':dashboard.$$panelDragging}">
+						<grafana-panel type="panel.type" ng-cloak></grafana-panel>
+					</div>
 
 
+					<div panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
+					</div>
 
 
-      <div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
-        <div class="span12" style="text-align:right;">
-          <span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
-            <span><i class="icon-plus-sign"></i> ADD A ROW</span>
-          </span>
-        </div>
-      </div>
-    </div>
-  </div>
+					<div class="clearfix"></div>
+				</div>
+			</div>
+		</div>
+
+		<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
+			<div class="span12" style="text-align:right;">
+				<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
+					<span><i class="icon-plus-sign"></i> ADD A ROW</span>
+				</span>
+			</div>
+		</div>
+
+	</div>
 
 
 	<div ng-include="'app/partials/console.html'" ng-if="consoleEnabled">
 	<div ng-include="'app/partials/console.html'" ng-if="consoleEnabled">
 	</div>
 	</div>

+ 12 - 3
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">
@@ -46,6 +49,9 @@
 						<li ng-show="isFavorite">
 						<li ng-show="isFavorite">
 							<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
 							<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
 						</li>
 						</li>
+						<li>
+							<a class="link" ng-click="editJson()">Dashboard JSON</a>
+						</li>
 						<li>
 						<li>
 							<a class="link" ng-click="exportDashboard()">Export dashboard</a>
 							<a class="link" ng-click="exportDashboard()">Export dashboard</a>
 						</li>
 						</li>
@@ -57,12 +63,15 @@
 					</ul>
 					</ul>
 				</li>
 				</li>
 
 
-				<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
+				<li class="dropdown grafana-menu-load">
+					<a bs-tooltip="'Search'" ng-click="openSearch()">
+						<i class='icon-folder-open'></i>
+					</a>
 				</li>
 				</li>
 
 
 				<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
 				<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
 
 
-				<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
+				<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" dash-editor-link="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
 
 
 				<li class="grafana-menu-stop-playlist hide">
 				<li class="grafana-menu-stop-playlist hide">
 					<a class='small' ng-click='stopPlaylist(2)'>
 					<a class='small' ng-click='stopPlaylist(2)'>

+ 98 - 93
src/app/partials/dasheditor.html

@@ -1,108 +1,113 @@
-<div class="modal-body">
-  <div class="pull-right editor-title">Dashboard settings</div>
+<div class="dashboard-editor-header">
+	<div class="dashboard-editor-title">
+		<i class="icon icon-cogs"></i>
+		Dashboard settings
+	</div>
 
 
-  <div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
-    <div ng-repeat="tab in ['General', 'Rows', 'Controls', 'Import']" data-title="{{tab}}">
-    </div>
-    <div ng-repeat="tab in dashboard.nav" data-title="{{tab.type}}">
-    </div>
-  </div>
+	<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+		<div ng-repeat="tab in ['General', 'Rows', 'Features', 'Import']" data-title="{{tab}}">
+		</div>
+		<div ng-repeat="tab in dashboard.nav" data-title="{{tab.type}}">
+		</div>
+	</div>
 
 
-  <div ng-if="editor.index == 0">
-    <div class="editor-row">
-      <div class="section">
-        <div class="editor-option">
-          <label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
-        </div>
-        <div class="editor-option">
-          <label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
-        </div>
-        <div class="editor-option">
-          <label class="small">Time correction</label>
-          <select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
-        </div>
-        <div class="editor-option">
-          <label class="small">Hide controls (CTRL+H)</label>
-          <input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
-        </div>
-     </div>
-    </div>
-     <div class="editor-row">
-      <div class="section">
-        <div class="editor-option">
-          <label class="small">Tags</label>
-          <bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
-          </bootstrap-tagsinput>
-          <tip>Press enter to a add tag</tip>
-        </div>
-      </div>
-    </div>
-  </div>
+</div>
 
 
-  <div ng-if="editor.index == 1">
-    <div class="editor-row">
-      <div class="span8">
-        <h4>Rows</h4>
-        <table class="table table-striped">
-          <thead>
-            <th width="1%"></th>
-            <th width="1%"></th>
-            <th width="1%"></th>
-            <th width="97%">Title</th>
-          </thead>
-          <tr ng-repeat="row in dashboard.rows">
-            <td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
-            <td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
-            <td><i ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="pointer icon-remove"></i></td>
-            <td>{{row.title}}</td>
-          </tr>
-        </table>
-      </div>
-      <div class="span4">
-        <h4>Add Row</h4>
-        <label class="small">Title</label>
-        <input type="text" class="input-normal" ng-model='row.title' placeholder="New row"></input>
-        <label class="small">Height</label>
-        <input type="text" class="input-mini" ng-model='row.height'></input>
-      </div>
-    </div>
-  </div>
+<div class="dashboard-editor-body">
 
 
-  <div ng-if="editor.index == 2">
-    <div class="editor-row">
-			<div class="section">
-				<h5>Feature toggles</h5>
-				<div class="editor-option" ng-repeat="pulldown in dashboard.pulldowns">
-					<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+		<div ng-if="editor.index == 0">
+			<div class="editor-row">
+				<div class="section">
+					<div class="editor-option">
+						<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
+					</div>
+					<div class="editor-option">
+						<label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
+					</div>
+					<div class="editor-option">
+						<label class="small">Time correction</label>
+						<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
+					</div>
+					<div class="editor-option">
+						<label class="small">Hide controls (CTRL+H)</label>
+						<input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
+					</div>
 				</div>
 				</div>
-				<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
-					<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+			</div>
+			<div class="editor-row">
+				<div class="section">
+					<div class="editor-option">
+						<label class="small">Tags</label>
+						<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
+						</bootstrap-tagsinput>
+						<tip>Press enter to a add tag</tip>
+					</div>
+
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
-	</div>
 
 
-	<div ng-if="editor.index == 3">
-    <ng-include src="'app/partials/import.html'"></ng-include>
-  </div>
+		<div ng-if="editor.index == 1">
+			<div class="editor-row">
+				<div class="span6">
+					<table class="grafana-options-table">
+						<tr ng-repeat="row in dashboard.rows">
+							<td style="width: 97%">
+								{{row.title}}
+							</td>
+							<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+							<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+							<td>
+								<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-mini">
+									<i class="icon-remove"></i>
+								</a>
+							</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</div>
 
 
-  <div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
-    <ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
-    <button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
-  </div>
+		<div ng-if="editor.index == 2">
+			<div class="editor-row">
+				<div class="section">
+					<div class="editor-option">
+						<label class="small">Templating</label>
+						<input type="checkbox" ng-model="dashboard.templating.enable" ng-checked="dashboard.templating.enable" ng-change="checkFeatureToggles()"x >
+					</div>
+					<div class="editor-option">
+						<label class="small">Annotations</label>
+						<input type="checkbox" ng-model="dashboard.annotations.enable" ng-checked="dashboard.annotations.enable" ng-change="checkFeatureToggles()">
+					</div>
+					<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
+						<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div ng-if="editor.index == 3">
+			<ng-include src="'app/partials/import.html'"></ng-include>
+		</div>
 
 
+		<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
+			<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
+			<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
+		</div>
+
+	</div>
+
+	<div class="clearfix"></div>
 </div>
 </div>
 
 
-<div class="modal-footer">
-  <div class="pull-left grafana-version-footer" ng-if="editor.index == 0">
-    <span class="editor-option small">
-      Grafana version: {{grafanaVersion}}
-    </span>
-		<span> | <a ng-click="toggleConsole()" ng-show="!consoleEnabled">enable console</a> <a ng-click="toggleConsole()" ng-show="consoleEnabled">disable console</a></span>
-    <div class="small" grafana-version-check>
-    </div>
-  </div>
+<div class="dashboard-editor-footer">
+	<div class="grafana-version-info" ng-show="editor.index === 0">
+		<span class="editor-option small">
+			Grafana version: {{grafanaVersion}} &nbsp;&nbsp;
+		</span>
+		<span grafana-version-check>
+		</span>
+	</div>
 
 
-  <button ng-click="add_row(dashboard,row); reset_row();" class="btn btn-success" ng-show="editor.index == 1">Create Row</button>
-  <button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();dashboard.emit_refresh()">Close</button>
+	<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss();reset_panel();dashboard.emit_refresh()">Close</button>
 </div>
 </div>

+ 19 - 0
src/app/partials/edit_json.html

@@ -0,0 +1,19 @@
+<div ng-controller="JsonEditorCtrl">
+
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-edit"></i>
+		  JSON
+		</div>
+	</div>
+
+	<div class="dashboard-editor-body" style="height: 500px">
+		<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%;"></textarea>
+	</div>
+
+	<div class="dashboard-editor-footer">
+		<button type="button" class="btn btn-success pull-left" ng-show="canUpdate" ng-click="update(); dismiss();">Update</button>
+		<button type="button" class="btn btn-success pull-right" ng-click="dismiss();">Close</button>
+	</div>
+
+</div>

+ 103 - 36
src/app/partials/graphite/editor.html

@@ -1,5 +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"
@@ -11,19 +10,19 @@
       <ul class="grafana-target-controls">
       <ul class="grafana-target-controls">
         <li ng-show="parserError">
         <li ng-show="parserError">
           <a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
           <a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
-            <i class="icon-warning-sign"></i>
+            <i class="icon icon-warning-sign"></i>
           </a>
           </a>
         </li>
         </li>
         <li>
         <li>
           <a class="pointer" tabindex="1" ng-click="showTextEditor = !showTextEditor">
           <a class="pointer" tabindex="1" ng-click="showTextEditor = !showTextEditor">
-            <i class="icon-pencil"></i>
+            <i class="icon icon-pencil"></i>
           </a>
           </a>
         </li>
         </li>
         <li class="dropdown">
         <li class="dropdown">
           <a  class="pointer dropdown-toggle"
           <a  class="pointer dropdown-toggle"
               data-toggle="dropdown"
               data-toggle="dropdown"
               tabindex="1">
               tabindex="1">
-            <i class="icon-cog"></i>
+            <i class="icon icon-cog"></i>
           </a>
           </a>
           <ul class="dropdown-menu pull-right" role="menu">
           <ul class="dropdown-menu pull-right" role="menu">
             <li role="menuitem">
             <li role="menuitem">
@@ -31,23 +30,19 @@
                   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>
           <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
           <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
-            <i class="icon-remove"></i>
+            <i class="icon icon-remove"></i>
           </a>
           </a>
         </li>
         </li>
       </ul>
       </ul>
 
 
-      <ul class="grafana-target-controls-left">
+      <ul class="grafana-segment-list">
+				<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
+					{{targetLetters[$index]}}
+        </li>
         <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();"
@@ -66,21 +61,8 @@
               ng-show="showTextEditor" />
               ng-show="showTextEditor" />
 
 
       <ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
       <ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
-        <li class="dropdown" ng-repeat="segment in segments" role="menuitem">
-          <a  tabindex="1"
-              class="grafana-target-segment dropdown-toggle"
-              data-toggle="dropdown"
-              ng-click="getAltSegments($index)"
-              focus-me="segment.focus"
-              ng-bind-html="segment.html">
-          </a>
-          <ul class="dropdown-menu scrollable grafana-segment-dropdown-menu" role="menu">
-            <li ng-repeat="altSegment in altSegments" role="menuitem">
-              <a href="javascript:void(0)" tabindex="1" ng-click="setSegment($index, $parent.$index)" ng-bind-html="altSegment.html"></a>
-            </li>
-          </ul>
-        </li>
-        <li ng-repeat="func in functions">
+        <li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
+				<li ng-repeat="func in functions">
           <span graphite-func-editor class="grafana-target-segment grafana-target-function">
           <span graphite-func-editor class="grafana-target-segment grafana-target-function">
           </span>
           </span>
         </li>
         </li>
@@ -91,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>
 
 
 
 

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

@@ -1,4 +1,4 @@
-<div ng-controller="GraphiteImportCtrl" ng-init="init()" style="height: 400px">
+<div ng-controller="GraphiteImportCtrl" ng-init="init()">
   <h5>Import dashboards from graphite web</h5>
   <h5>Import dashboards from graphite web</h5>
 
 
   <div class="editor-row">
   <div class="editor-row">
@@ -12,7 +12,7 @@
           </li>
           </li>
         </ul>
         </ul>
       </div>
       </div>
-      <button ng-click="listAll()" class="btn btn-primary">List all dashboards</button>
+      <button ng-click="listAll()" class="btn btn-success">List all dashboards</button>
     </div>
     </div>
   </div>
   </div>
 
 

+ 2 - 2
src/app/partials/influxdb/annotation_editor.html

@@ -1,8 +1,8 @@
 <div class="editor-row">
 <div class="editor-row">
 	<div class="section">
 	<div class="section">
-		<h5>InfluxDB Query <tip>Example: select text from events where [[timeFilter]]</tip></h5>
+		<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
 		<div class="editor-option">
 		<div class="editor-option">
-			<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where [[timeFilter]]"></input>
+			<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where $timeFilter"></input>
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>

+ 184 - 106
src/app/partials/influxdb/editor.html

@@ -1,5 +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"
@@ -14,7 +13,7 @@
             <a class="pointer dropdown-toggle"
             <a class="pointer dropdown-toggle"
                data-toggle="dropdown"
                data-toggle="dropdown"
                tabindex="1">
                tabindex="1">
-              <i class="icon-cog"></i>
+              <i class="icon icon-cog"></i>
             </a>
             </a>
             <ul class="dropdown-menu pull-right" role="menu">
             <ul class="dropdown-menu pull-right" role="menu">
               <li role="menuitem">
               <li role="menuitem">
@@ -26,16 +25,14 @@
           </li>
           </li>
           <li>
           <li>
             <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
             <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
-              <i class="icon-remove"></i>
+              <i class="icon icon-remove"></i>
             </a>
             </a>
           </li>
           </li>
         </ul>
         </ul>
 
 
-        <ul class="grafana-target-controls-left">
+        <ul class="grafana-segment-list">
           <li>
           <li>
-            <a class="grafana-target-segment"
-               ng-click="target.hide = !target.hide; get_data();"
-               role="menuitem">
+            <a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
               <i class="icon-eye-open"></i>
               <i class="icon-eye-open"></i>
             </a>
             </a>
           </li>
           </li>
@@ -45,7 +42,7 @@
 				<ul class="grafana-segment-list" ng-show="target.rawQuery">
 				<ul class="grafana-segment-list" ng-show="target.rawQuery">
 					<li>
 					<li>
 						<input type="text"
 						<input type="text"
-               class="grafana-target-segment-input span8"
+               class="grafana-target-text-input span10"
                ng-model="target.query"
                ng-model="target.query"
                placeholder="select ..."
                placeholder="select ..."
                focus-me="target.rawQuery"
                focus-me="target.rawQuery"
@@ -54,26 +51,16 @@
                ng-model-onblur
                ng-model-onblur
 							 ng-blur="get_data()">
 							 ng-blur="get_data()">
 					</li>
 					</li>
-
-					<li class="grafana-target-segment">
-            as
-          </li>
-
-          <li>
-            <input type="text"
-                   class="input-medium grafana-target-segment-input"
-                   ng-model="target.alias"
-                   spellcheck='false'
-                   placeholder="alias"
-                   ng-blur="get_data()">
-          </li>
 				</ul>
 				</ul>
 
 
 				<!-- Query editor mode -->
 				<!-- Query editor mode -->
         <ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
         <ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
+					<li class="grafana-target-segment">
+						series
+					</li>
           <li>
           <li>
             <input type="text"
             <input type="text"
-                   class="input-large grafana-target-segment-input"
+                   class="grafana-target-text-input span8"
                    ng-model="target.series"
                    ng-model="target.series"
                    spellcheck='false'
                    spellcheck='false'
                    bs-typeahead="listSeries"
                    bs-typeahead="listSeries"
@@ -82,107 +69,198 @@
                    ng-blur="seriesBlur()">
                    ng-blur="seriesBlur()">
           </li>
           </li>
 
 
-          <li class="grafana-target-segment">
-            select
-          </li>
-
-          <li class="dropdown">
-            <span influxdb-func-editor class="grafana-target-segment grafana-target-function">
-            </span>
-          </li>
+					<li class="grafana-target-segment">
+						alias
+					</li>
 
 
-          <li>
-            <a class="grafana-target-segment"
-               ng-click="target.condition_filter = !target.condition_filter; get_data();"
-               bs-tooltip="'Add a where clause'"
-               role="menuitem">
-              <i class="icon-filter"></i>
-            </a>
-          </li>
-          <li ng-show="target.condition_filter">
-            <input type="text"
-                   class="input-small grafana-target-segment-input"
-                   ng-model="target.condition_key"
-                   placeholder="key"
-                   spellcheck='false'
-                   bs-typeahead="listColumns"
-                   data-min-length=0
-                   ng-blur="get_data()">
-            <select class="input-mini grafana-target-segment-input"
-                    ng-change="get_data()"
-                    ng-model="target.condition_op"
-                    ng-options="f for f in operators" ></select>
-            <input type="text"
-                   class="input-small grafana-target-segment-input"
-                   ng-model="target.condition_value"
-                   placeholder="value"
-                   spellcheck='false'
-                   data-min-length=0
-                   ng-blur="get_data()">
-          </li>
-          <li class="grafana-target-segment">
-            group by time
-          </li>
+					<li>
+						<input type="text" class="input-medium grafana-target-text-input" ng-model="target.alias"
+						spellcheck='false' placeholder="alias" ng-blur="get_data()">
+					</li>
 
 
-          <li>
-            <input type="text"
-                   class="input-mini grafana-target-segment-input"
-                   ng-model="target.interval"
-                   placeholder="{{interval}}"
-                   bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
-                   spellcheck='false'
-                   ng-model-onblur ng-change="get_data()" >
-          </li>
-          <li>
-            <a class="grafana-target-segment"
-               ng-click="target.groupby_field_add = !target.groupby_field_add; get_data();"
-               bs-tooltip="'Add a group by column'"
-               role="menuitem">
-              <i class="icon-plus"></i>
-            </a>
-          </li>
+        </ul>
 
 
-          <li ng-show="target.groupby_field_add">
-            <input type="text"
-                   class="input-small grafana-target-segment-input"
-                   ng-model="target.groupby_field"
-                   placeholder="column"
-                   spellcheck="false"
-                   bs-typeahead="listColumns"
-                   data-min-length=0
-                   ng-blur="get_data()">
-          </li>
+        <div class="clearfix"></div>
+      </div>
 
 
-          <li class="grafana-target-segment">
-            as
+      <div class="grafana-target-inner">
+				<!-- Raw Query mode  -->
+				<ul class="grafana-segment-list" ng-show="target.rawQuery">
+					<li class="grafana-target-segment">
+						<i class="icon-eye-open invisible"></i>
+					</li>
+					<li class="grafana-target-segment">
+            alias
           </li>
           </li>
-
           <li>
           <li>
             <input type="text"
             <input type="text"
-                   class="input-medium grafana-target-segment-input"
+                   class="input-medium grafana-target-text-input"
                    ng-model="target.alias"
                    ng-model="target.alias"
                    spellcheck='false'
                    spellcheck='false'
                    placeholder="alias"
                    placeholder="alias"
                    ng-blur="get_data()">
                    ng-blur="get_data()">
           </li>
           </li>
+					<li class="grafana-target-segment">
+						group by time
+					</li>
+					<li>
+						<input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
+									 spellcheck='false' placeholder="{{interval}}" data-placement="right"
+									 bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
+									 ng-model-onblur ng-change="get_data()" >
+					</li>
+				</ul>
+
+				<!-- Query editor mode -->
+        <ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
+					<li class="grafana-target-segment">
+						<i class="icon-eye-open invisible"></i>
+					</li>
+					<li class="grafana-target-segment">
+						select
+					</li>
+					<li class="dropdown">
+						<span influxdb-func-editor class="grafana-target-segment">
+						</span>
+					</li>
+
+					<li class="grafana-target-segment">
+						where
+					</li>
+					<li>
+						<input type="text" class="input-medium grafana-target-text-input" ng-model="target.condition"
+									 bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
+					</li>
+
+					<li class="grafana-target-segment">
+						group by time
+					</li>
+					<li>
+						<input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
+									 spellcheck='false' placeholder="{{interval}}" data-placement="right"
+									 bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
+									 ng-model-onblur ng-change="get_data()" >
+					</li>
+
+					<li class="grafana-target-segment">
+							<i class="icon-plus"></i>
+					</li>
+
+					<li>
+						<input type="text" class="input-small grafana-target-text-input" ng-model="target.groupby_field" bs-tooltip="'Add a group by column or leave blank'"
+									 placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()">
+					</li>
+
+					<li class="dropdown">
+						<a class="grafana-target-segment pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
+							<span ng-show="target.fill">
+								fill ({{target.fill}})
+							</span>
+							<span ng-show="!target.fill">
+								no fill
+							</span>
+					  </a>
+						<ul class="dropdown-menu">
+							<li><a ng-click="target.fill = ''">no fill</a></li>
+							<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
+							<li><a ng-click="target.fill = '0'">fill (0)</a></li>
+						</ul>
+					</li>
+
         </ul>
         </ul>
 
 
         <div class="clearfix"></div>
         <div class="clearfix"></div>
       </div>
       </div>
-    </div>
-  </div>
 
 
-  <div class="pull-left metrics-editor-help" style="margin-top: 30px;">
-    <div class="span6">
-      <span class="pointer">
-        <i class="icon-question-sign"></i> alias patterns:
-      </span>
-      <ul class="hide">
-        <li>$s = series name</li>
-        <li>$g = group by</li>
-        <li>$[0-9] part of series name for series names seperated by dots.</li>
-      <ul>
     </div>
     </div>
   </div>
   </div>
+
+</div>
+
+<section class="grafana-metric-options">
+	<div class="grafana-target">
+		<div class="grafana-target-inner">
+			<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">
+					group by time
+				</li>
+				<li>
+					<input type="text" class="input-medium grafana-target-text-input" ng-model="panel.interval" ng-blur="get_data();"
+					       spellcheck='false' placeholder="example: >10s">
+				</li>
+				<li class="grafana-target-segment">
+					<i class="icon-question-sign" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
+				</li>
+			</ul>
+			<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">
+						alias patterns
+					</a>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						stacking &amp; and fill
+					</a>
+				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						group by time
+					</a>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+	</div>
+</section>
+
+<div class="editor-row">
+	<div class="pull-left" style="margin-top: 30px;">
+
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
+			<h5>Alias patterns</h5>
+			<ul>
+				<li>$s = series name</li>
+				<li>$g = group by</li>
+				<li>$[0-9] part of series name for series names seperated by dots.</li>
+			</ul>
+		</div>
+
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
+			<h5>Stacking and fill</h5>
+			<ul>
+				<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>You must use fill(0), and select a group by time low limit</li>
+				<li>Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds</li>
+				<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
+			</ul>
+		</div>
+
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
+			<h5>Group by time</h5>
+			<ul>
+				<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>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
+				<li>The low limit can only be set in the group by time option below your queries</li>
+				<li>You set a low limit by adding a greater sign before the interval</li>
+				<li>Example: &gt;60s if you write metrics to InfluxDB every 60 seconds</li>
+			</ul>
+		</div>
+
+
+	</div>
 </div>
 </div>
 
 
+

+ 65 - 54
src/app/partials/inspector.html

@@ -1,69 +1,80 @@
 <div class="modal-body" ng-controller="InspectCtrl" ng-init="init()">
 <div class="modal-body" ng-controller="InspectCtrl" ng-init="init()">
-  <div class="pull-right editor-title">Inspector</div>
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-eye-open"></i>
+			Inspector
+		</div>
 
 
-  <div ng-model="editor.index" bs-tabs>
-    <div ng-repeat="tab in ['Request', 'Response', 'JS Error']" data-title="{{tab}}">
-    </div>
-  </div>
+		<div ng-model="editor.index" bs-tabs>
+			<div ng-repeat="tab in ['Request', 'Response', 'JS Error']" data-title="{{tab}}">
+			</div>
+		</div>
 
 
-  <div ng-if="editor.index == 0">
-    <h5>Request details</h5>
-    <table class="table table-striped small inspector-request-table">
-      <tr>
-        <td>Url</td>
-        <td>{{inspector.error.config.url}}</td>
-      </tr>
-      <tr>
-        <td>Method</td>
-        <td>{{inspector.error.config.method}}</td>
-      </tr>
-      <tr ng-repeat="(key, value) in inspector.error.config.headers">
-        <td>
-          {{key}}
-        </td>
-        <td>
-          {{value}}
-        </td>
-      </tr>
-    </table>
+	</div>
 
 
-    <h5>Request parameters</h5>
-    <table class="table table-striped small inspector-request-table">
-        <tr ng-repeat="param in request_parameters">
-          <td>
-            {{param.key}}
-          </td>
-          <td>
-            {{param.value}}
-          </td>
-        </tr>
-    </table>
-  </div>
+	<div class="dashboard-editor-body">
 
 
-  <div ng-if="editor.index == 1">
-		<h5 ng-if="response" ng-bind="response"></h5>
+		<div ng-if="editor.index == 0">
+			<h5>Request details</h5>
+			<table class="table table-striped small inspector-request-table">
+				<tr>
+					<td>Url</td>
+					<td>{{inspector.error.config.url}}</td>
+				</tr>
+				<tr>
+					<td>Method</td>
+					<td>{{inspector.error.config.method}}</td>
+				</tr>
+				<tr ng-repeat="(key, value) in inspector.error.config.headers">
+					<td>
+						{{key}}
+					</td>
+					<td>
+						{{value}}
+					</td>
+				</tr>
+			</table>
 
 
-    <div ng-if="response_html">
-      <div iframe-content="response_html"></div>
-    </div>
+			<h5>Request parameters</h5>
+			<table class="table table-striped small inspector-request-table">
+				<tr ng-repeat="param in request_parameters">
+					<td>
+						{{param.key}}
+					</td>
+					<td>
+						{{param.value}}
+					</td>
+				</tr>
+			</table>
+		</div>
 
 
-  </div>
+		<div ng-if="editor.index == 1">
+			<h5 ng-if="response" ng-bind="response"></h5>
 
 
-  <div ng-if="editor.index == 2">
+			<div ng-if="response_html">
+				<div iframe-content="response_html"></div>
+			</div>
 
 
-    <label>Message:</label>
-    <pre>
-      {{message}}
-    </pre>
+		</div>
 
 
-    <label>Stack trace:</label>
-    <pre>
-      {{stack_trace}}
-    </pre>
+		<div ng-if="editor.index == 2">
 
 
-  </div>
+			<label>Message:</label>
+			<pre>
+			{{message}}
+		</pre>
+
+			<label>Stack trace:</label>
+			<pre>
+			{{stack_trace}}
+		</pre>
+
+		</div>
+
+	</div>
 
 
 </div>
 </div>
+
 <div class="modal-footer">
 <div class="modal-footer">
-  <button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
+	<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
 </div>
 </div>

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

@@ -12,7 +12,7 @@
             <a  class="pointer dropdown-toggle"
             <a  class="pointer dropdown-toggle"
                 data-toggle="dropdown"
                 data-toggle="dropdown"
                 tabindex="1">
                 tabindex="1">
-              <i class="icon-cog"></i>
+              <i class="icon icon-cog"></i>
             </a>
             </a>
             <ul class="dropdown-menu pull-right" role="menu">
             <ul class="dropdown-menu pull-right" role="menu">
               <li role="menuitem">
               <li role="menuitem">
@@ -25,12 +25,12 @@
           </li>
           </li>
           <li>
           <li>
             <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
             <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
-              <i class="icon-remove"></i>
+              <i class="icon icon-remove"></i>
             </a>
             </a>
           </li>
           </li>
         </ul>
         </ul>
 
 
-        <ul class="grafana-target-controls-left">
+        <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>

+ 26 - 19
src/app/partials/paneleditor.html

@@ -1,23 +1,30 @@
-<div bindonce class="modal-body">
-  <div class="pull-right editor-title" bo-text="panel.type+' settings'"></div>
-  <div ng-model="editor.index" bs-tabs>
-    <div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
-    </div>
-  </div>
-  <div ng-show="editorTabs[editor.index] == 'General'">
-    <div ng-include src="'app/partials/panelgeneral.html'"></div>
-  </div>
+<div bindonce class="dashboard-editor-header">
+	<div class="dashboard-editor-title">
+		<i class="icon icon-text-width"></i>
+		<span bo-text="panel.type+' settings'"></span>
+	</div>
 
 
-  <div ng-show="editorTabs[editor.index] == 'Panel'">
-    <div ng-include src="edit_path(panel.type)"></div>
-  </div>
+	<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+		<div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
+		</div>
+	</div>
 
 
-  <div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
-    <div ng-include src="tab.src"></div>
-  </div>
 </div>
 </div>
 
 
-<div class="modal-footer">
-  <!-- close_edit() is provided here to allow for a scope to perform action on dismiss -->
-  <button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss()">Close</button>
-</div>
+<div class="dashboard-editor-body">
+	<div ng-show="editorTabs[editor.index] == 'General'">
+		<div ng-include src="'app/partials/panelgeneral.html'"></div>
+	</div>
+
+	<div ng-show="editorTabs[editor.index] == 'Panel'">
+		<div ng-include src="edit_path(panel.type)"></div>
+	</div>
+
+	<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
+		<div ng-include src="tab.src"></div>
+	</div>
+</div>
+
+<div class="dashboard-editor-footer">
+	<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss()">Close</button>
+</div>

+ 5 - 2
src/app/partials/panelgeneral.html

@@ -4,8 +4,11 @@
     <div class="editor-option">
     <div class="editor-option">
       <label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
       <label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
     </div>
     </div>
-    <div class="editor-option" ng-hide="panel.sizeable == false">
+    <div class="editor-option">
       <label class="small">Span</label> <select class="input-mini" ng-model="panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
       <label class="small">Span</label> <select class="input-mini" ng-model="panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
+    </div>
+		<div class="editor-option">
+      <label class="small">Height</label><input type="text" class="input-small" ng-model='panel.height'></select>
     </div>
     </div>
   </div>
   </div>
-</div>
+</div>

+ 60 - 54
src/app/partials/playlist.html

@@ -1,55 +1,61 @@
 <div ng-controller="PlaylistCtrl" ng-init="init()">
 <div ng-controller="PlaylistCtrl" ng-init="init()">
-  <div class="modal-header">
-    <h3>Start dashboard playlist</h3>
-  </div>
-  <div class="modal-body">
-    <div class="editor-row">
-      <div class="section">
-        <div class="editor-option">
-          <table class="table table-striped span4">
-            <tr>
-              <th>Dashboard</th>
-              <th>Include</th>
-              <th style="white-space: nowrap;">Remove as favorite</th>
-            </tr>
-            <tr ng-repeat="dashboard in favDashboards">
-              <td style="white-space: nowrap;">
-                {{dashboard.title}}
-              </td>
-              <td style="text-align: center">
-                <input type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
-              </td>
-              <td style="text-align: center">
-                <i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>
-              </td>
-            </tr>
-            <tr ng-hide="favDashboards.length">
-              <td colspan="3">
-                <i class="icon-warning"></i> No dashboards marked as favorites
-              </td>
-            </tr>
-          </table>
-        </div>
-        <div class="editor-option">
-          <div class="span4">
-            <span><i class="icon-question-sign"></i>
-              Dashboards available in the playlist are only the once 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
-              <br/><br/>
-            </span>
-          </div>
-        </div>
-        <div class="editor-option">
-          <label>
-            Timespan between change
-          </label>
-          <input type="text" class="input-small" ng-model="timespan" />
-        </div>
-      </div>
-    </div>
-    <div class="modal-footer">
-      <button class="btn btn-success" ng-click="start();dismiss();"><i class="icon-play"></i> Start</button>
-      <button type="button" class="btn btn-primary" ng-click="dismiss();"><i class="icon-ban-circle"></i> Cancel</button>
-    </div>
-  </div>
-</div>
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-play"></i>
+			Start dashboard playlist
+		</div>
+	</div>
+
+	<div class="dashboard-editor-body">
+
+		<div class="editor-row">
+			<div class="section">
+				<div class="editor-option">
+					<table class="table table-striped span4">
+						<tr>
+							<th>Dashboard</th>
+							<th>Include</th>
+							<th style="white-space: nowrap;">Remove as favorite</th>
+						</tr>
+						<tr ng-repeat="dashboard in favDashboards">
+							<td style="white-space: nowrap;">
+								{{dashboard.title}}
+							</td>
+							<td style="text-align: center">
+								<input type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
+							</td>
+							<td style="text-align: center">
+								<i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>
+							</td>
+						</tr>
+						<tr ng-hide="favDashboards.length">
+							<td colspan="3">
+								<i class="icon-warning"></i> No dashboards marked as favorites
+							</td>
+						</tr>
+					</table>
+				</div>
+				<div class="editor-option">
+					<div class="span4">
+						<span><i class="icon-question-sign"></i>
+							dashboards available in the playlist are only the once 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
+							<br/><br/>
+						</span>
+					</div>
+				</div>
+				<div class="editor-option">
+					<label>
+						Timespan between change
+					</label>
+					<input type="text" class="input-small" ng-model="timespan" />
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<div class="dashboard-editor-footer">
+		<button class="btn btn-success" ng-click="start();dismiss();"><i class="icon-play"></i> Start</button>
+		<button type="button" class="btn btn-success pull-right" ng-click="dismiss();"><i class="icon-ban-circle"></i> Close</button>
+	</div>
+</div>

+ 33 - 24
src/app/partials/roweditor.html

@@ -1,12 +1,19 @@
-<div class="modal-body">
-  <div class="pull-right editor-title">Row settings</div>
+<div class="dashboard-editor-header">
+	<div class="dashboard-editor-title">
+		<i class="icon icon-th-list"></i>
+		Row settings
+	</div>
 
 
-  <div ng-model="editor.index" bs-tabs>
+	<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
     <div ng-repeat="tab in ['General','Panels']" data-title="{{tab}}">
     <div ng-repeat="tab in ['General','Panels']" data-title="{{tab}}">
-    </div>
-  </div>
+		</div>
+	</div>
+
+</div>
 
 
-  <div class="editor-row" ng-if="editor.index == 0">
+<div class="dashboard-editor-body">
+
+ <div class="editor-row" ng-if="editor.index == 0">
     <div class="editor-option">
     <div class="editor-option">
       <label class="small">Title</label><input type="text" class="input-medium" ng-model='row.title'></input>
       <label class="small">Title</label><input type="text" class="input-medium" ng-model='row.title'></input>
     </div>
     </div>
@@ -22,30 +29,32 @@
 	</div>
 	</div>
   <div class="row-fluid" ng-if="editor.index == 1">
   <div class="row-fluid" ng-if="editor.index == 1">
     <div class="span12">
     <div class="span12">
-      <h4>Panels</h4>
-      <table class="table table-condensed table-striped">
+			<table class="grafana-options-table" style="max-width: 400px; width: auto">
         <thead>
         <thead>
           <th>Title</th>
           <th>Title</th>
           <th>Type</th>
           <th>Type</th>
-          <th>Span <span class="small">({{dashboard.rowSpan(row)}}/12)</span></th>
-          <th>Delete</th>
-          <th>Move</th>
+          <th>Span</span></th>
+          <th></th>
           <th></th>
           <th></th>
+					<th></th>
         </thead>
         </thead>
         <tr ng-repeat="panel in row.panels">
         <tr ng-repeat="panel in row.panels">
-          <td>{{panel.title}}</td>
+          <td style="width: 95%">{{panel.title}}</td>
           <td>{{panel.type}}</td>
           <td>{{panel.type}}</td>
-          <td><select ng-hide="panel.sizeable == false" class="input-mini" ng-model="panel.span" ng-options="size for size in [1,2,3,4,5,6,7,8,9,10,11,12]"></select></td>
-          <td><i ng-click="row.panels = _.without(row.panels,panel)" class="pointer icon-remove"></i></td>
-          <td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
-          <td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
-        </tr>
-      </table>
-    </div>
-  </div>
+          <td><select ng-hide="panel.sizeable == false" class="input-mini" style="margin-bottom: 0;" ng-model="panel.span" ng-options="size for size in [1,2,3,4,5,6,7,8,9,10,11,12]"></select></td>
+					<td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+					<td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+					<td>
+						<a ng-click="row.panels = _.without(row.panels,panel)" class="btn btn-danger btn-mini">
+							<i class="icon-remove"></i>
+						</a>
+					</td>
+				</tr>
+			</table>
+		</div>
+	</div>
 </div>
 </div>
-<div class="modal-footer">
-  <button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
-  <button ng-show="panel.type && editor.index == 2" ng-click="add_panel(panel); reset_panel(); editor.index = 1;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
-  <button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
+
+<div class="dashboard-editor-footer">
+	<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
 </div>
 </div>

+ 78 - 95
src/app/partials/search.html

@@ -1,105 +1,88 @@
-  <style>
-    #grafana-search {
-      position: fixed;
-      right: 0;
-      left: 0;
-      top: 39px;
-      margin-right: auto;
-      margin-left: auto;
-      /* give it dimensions */
-      min-height: 10em;
-      width: 90%;
-    }
-  </style>
+<div ng-controller="SearchCtrl" ng-init="init()">
 
 
-  <a href="#" bs-tooltip="'Search'" data-placement="bottom" ng-click="openSearch()" class="dropdown-toggle" data-toggle="dropdown">
-    <i class='icon-folder-open'></i>
-  </a>
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title" style="border: 0; line-height: 41px;">
+			<i class="icon icon-search"></i>
+			Search
+		</div>
 
 
-  <ul class="dropdown-menu" id="grafana-search" ng-if="searchOpened">
-    <li ng-if="!showImport">
-      <div class="grafana-search-panel">
-        <div class="search-field-wrapper">
-          <button class="btn btn-success pull-right" config-modal="app/partials/playlist.html">
-            <i class="icon-play"></i>
-            Playlist
-          </button>
-          <button class="btn btn-success pull-right" ng-click="toggleImport($event)">
-            <i class="icon-download-alt"></i>
-            Import
-          </button>
-          <button class="btn btn-success pull-right" ng-click="newDashboard()">
-            <i class="icon-th-large"></i>
-            New
-          </button>
-          <span class="position: relative;">
-            <input  type="text"
-                    placeholder="search dashboards, metrics, or graphs"
-                    xng-focus="giveSearchFocus"
-                    ng-keydown="keyDown($event)"
-                    ng-model="query.query" spellcheck='false'
-                    ng-change="search()" />
-            <a class="search-tagview-switch" href="javascript:void(0);"
-              ng-class="{'active': tagsOnly}"
-              ng-click="showTags($event)">tags</a>
-          </span>
-        </div>
+		<div class="grafana-search-panel">
+			<div class="search-field-wrapper">
+				<button class="btn btn-success pull-right" dash-editor-link="app/partials/playlist.html" editor-scope="isolated">
+					<i class="icon-play"></i>
+					Playlist
+				</button>
+				<button class="btn btn-success pull-right" ng-click="toggleImport($event)">
+					<i class="icon-download-alt"></i>
+					Import
+				</button>
+				<button class="btn btn-success pull-right" ng-click="newDashboard()">
+					<i class="icon-th-large"></i>
+					New
+				</button>
+				<span style="position: relative;">
+					<input  type="text" placeholder="search dashboards, metrics, or graphs" xng-focus="giveSearchFocus"
+									ng-keydown="keyDown($event)" ng-model="query.query" spellcheck='false' ng-change="search()" />
+					<a class="search-tagview-switch" href="javascript:void(0);" ng-class="{'active': tagsOnly}" ng-click="showTags($event)">tags</a>
+				</span>
+			</div>
+		</div>
+	</div>
 
 
-        <h6 ng-hide="results.dashboards.length">No dashboards matching your query were found.</h6>
+	<div ng-if="!showImport">
+		<h6 ng-hide="results.dashboards.length">No dashboards matching your query were found.</h6>
+		<div class="search-results-container" ng-if="tagsOnly">
+			<div class="row">
+				<div class="span6 offset1">
+			<div ng-repeat="tag in results.tags" class="pointer" style="width: 180px; float: left;"
+				ng-class="{'selected': $index === selectedIndex }"
+				ng-click="filterByTag(tag.term, $event)">
+				<a class="search-result-tag label label-tag" tag-color-from-name>
+					<i class="icon icon-tag"></i>
+					<span>{{tag.term}} &nbsp;({{tag.count}})</span>
+				</a>
+			</div>
+			</div>
+			</div>
+		</div>
 
 
-        <div class="search-results-container" ng-if="tagsOnly">
-					<div ng-repeat="tag in results.tags"
-						   class="search-result-item pointer"
-							 ng-class="{'selected': $index === selectedIndex }"
-							 ng-click="filterByTag(tag.term, $event)">
-						<a class="search-result-link" >
-							<i class="icon icon-tag"></i>
-							<span class="label label-tag">{{tag.term}} &nbsp;({{tag.count}})</span>
-						</a>
-          </div>
-        </div>
+		<div class="search-results-container" ng-if="!tagsOnly">
+			<div class="search-result-item pointer" bindonce ng-repeat="row in results.dashboards"
+				ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
 
 
-				<div class="search-results-container" ng-if="!tagsOnly">
-					<div class="search-result-item pointer"
-							bindonce ng-repeat="row in results.dashboards"
-							ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
-
-							<div class="search-result-actions">
-								<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
-									<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
-								</a>
-								<a ng-click="deleteDashboard(row.id, $event)">
-									<i class="icon-remove"></i> delete
-								</a>
-							</div>
+				<div class="search-result-actions small">
+					<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
+						<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
+					</a>
+					<a ng-click="deleteDashboard(row.id, $event)">
+						<i class="icon-remove"></i> delete
+					</a>
+				</div>
 
 
-							<div class="search-result-tags">
-								<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" style="margin-right: 5px;" class="label label-tag">
-									{{tag}}
-								</a>
-							</div>
+				<div class="search-result-tags">
+					<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name  class="label label-tag">
+						{{tag}}
+					</a>
+				</div>
 
 
-							<a class="search-result-link">
-								<i class="icon icon-th-large"></i>
-								<span bo-text="row.title"></span>
-							</a>
+				<a class="search-result-link">
+					<i class="icon icon-th-large"></i>
+					<span bo-text="row.title"></span>
+				</a>
 
 
-					</div>
-				</div>
+			</div>
+		</div>
+	</div>
 
 
-      </div>
-    </li>
+	<div class="editor-row" ng-if="showImport">
+		<div class="section">
+			<div class="editor-option">
+				<h5>Local File <tip>Load dashboard JSON layout from file</tip></h5>
+				<form>
+					<input type="file" id="dashupload" dash-upload/><br>
+				</form>
+			</div>
+		</div>
+	</div>
 
 
-    <li ng-if="showImport" style="margin: 20px;">
-      <div class="editor-row">
-        <div class="section">
-          <div class="editor-option">
-            <h5>Local File <tip>Load dashboard JSON layout from file</tip></h5>
-            <form>
-              <input type="file" id="dashupload" dash-upload /><br>
-            </form>
-          </div>
-        </div>
-      </div>
-    </li>
-  </ul>
+</div>

+ 49 - 0
src/app/partials/submenu.html

@@ -0,0 +1,49 @@
+<div class="submenu-controls" ng-controller="SubmenuCtrl">
+		<div class="grafana-target">
+			<div class="grafana-target-inner" style="border-top: none">
+
+				<ul class="grafana-segment-list">
+					<li class="grafana-target-segment">
+						<div class="dropdown">
+							<a class="pointer" data-toggle="dropdown">
+								<i class="icon-cog"></i>
+							</a>
+							<ul class="dropdown-menu">
+								<li><a class="pointer" dash-editor-link="app/partials/templating_editor.html">Templating</a></li>
+								<li><a class="pointer" dash-editor-link="app/partials/annotations_editor.html">Annotations</a></li>
+							</ul>
+						</div>
+					</li>
+				</ul>
+
+				<ul class="grafana-segment-list" ng-if="dashboard.templating.enable">
+					<li class="small grafana-target-segment">
+						<strong>VARIABLES</strong>
+					</li>
+					<li ng-repeat-start="variable in variables" class="grafana-target-segment template-param-name">
+						<span class="template-variable ">
+						${{variable.name}}:
+						</span>
+					</li>
+
+					<li ng-repeat-end template-param-selector>
+					</li>
+				</ul>
+
+				<ul class="grafana-segment-list" ng-if="dashboard.annotations.enable">
+					<li class="small grafana-target-segment">
+						<strong>ANNOTATIONS</strong>
+					</li>
+
+					<li ng-repeat="annotation in dashboard.annotations.list" class="grafana-target-segment annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
+						<a ng-click="disableAnnotation(annotation)">
+							<i class="annotation-color-icon icon-bolt"></i>
+							{{annotation.name}}
+						</a>
+					</li>
+				</ul>
+
+				<div class="clearfix"></div>
+			</div>
+		</div>
+	</div>

+ 170 - 0
src/app/partials/templating_editor.html

@@ -0,0 +1,170 @@
+<div ng-controller="TemplateEditorCtrl" ng-init="init()"> <div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-code"></i>
+			Templating
+		</div>
+
+		<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+			<div ng-repeat="tab in ['Variables', 'Add', 'Edit']" data-title="{{tab}}">
+			</div>
+		</div>
+
+	</div>
+
+	<div class="dashboard-editor-body">
+
+		<div ng-if="editor.index == 0">
+
+			<div class="editor-row row">
+				<div class="span8">
+					<div ng-if="variables.length === 0">
+						<em>No template variables defined</em>
+					</div>
+					<table class="grafana-options-table">
+						<tr ng-repeat="variable in variables">
+							<td style="width: 1%">
+								<span class="template-variable">
+									${{variable.name}}
+								</span>
+							</td>
+							<td class="max-width" style="max-width: 200px;">
+								{{variable.query}}
+							</td>
+							<td style="width: 1%">
+								<a ng-click="edit(variable)" class="btn btn-success btn-mini">
+									<i class="icon-edit"></i>
+									Edit
+								</a>
+							</td>
+							<td style="width: 1%"><i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+							<td style="width: 1%"><i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+							<td style="width: 1%">
+								<a ng-click="removeVariable(variable)" class="btn btn-danger btn-mini">
+									<i class="icon-remove"></i>
+								</a>
+							</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+
+		</div>
+
+		<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
+			<div class="row">
+				<div class="editor-option">
+					<div class="editor-row">
+						<div class="editor-option">
+							<label class="small">Variable name</label>
+							<input type="text" class="input-medium" ng-model='current.name' placeholder="name"></input>
+						</div>
+						<div class="editor-option">
+							<label class="small">Type</label>
+							<select class="input-medium" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
+						</div>
+						<div class="editor-option" ng-show="current.type === 'query'">
+							<label class="small">Datasource</label>
+							<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+						</div>
+						<div class="editor-option text-center" ng-show="current.type === 'query'">
+							<label class="small">Refresh on load <tip>Check if you want values to be updated on dashboard load, will slow down dashboard load time.</tip></label>
+							<input type="checkbox" ng-model="current.refresh" ng-checked="current.refresh">
+						</div>
+					</div>
+
+					<div ng-show="current.type === 'interval'">
+						<div class="editor-row">
+							<div class="editor-option">
+								<label class="small">Values</label>
+								<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="name"></input>
+							</div>
+						</div>
+						<div class="editor-row">
+							<div class="editor-option text-center">
+								<label class="small">Include auto interval</label>
+								<input type="checkbox" ng-model="current.auto" ng-checked="current.auto" ng-change="runQuery()">
+							</div>
+							<div class="editor-option" ng-show="current.auto">
+								<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
+								<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
+							</div>
+						</div>
+					</div>
+
+					<div ng-show="current.type === 'custom'">
+						<div class="editor-row">
+							<div class="editor-option">
+								<label class="small">Values seperated by comma</label>
+								<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
+							</div>
+						</div>
+					</div>
+
+					<div ng-show="current.type === 'query'">
+						<div class="editor-row">
+							<div class="editor-option form-inline">
+								<label class="small">Variable values query</label>
+								<input type="text" class="input-xxlarge" ng-model='current.query' placeholder="apps.servers.*"></input>
+								<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'Execute query'" data-placement="right"><i class="icon-play"></i></button>
+							</div>
+						</div>
+
+						<div class="editor-row" style="margin: 15px 0">
+							<div class="editor-option form-inline">
+								<label class="small">regex (optional, if you want to extract part of a series name or metric node segment)</label>
+								<input type="text" class="input-xxlarge" ng-model='current.regex' placeholder="/.*-(.*)-.*/"></input>
+								<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'execute query'" data-placement="right"><i class="icon-play"></i></button>
+							</div>
+						</div>
+
+						<div class="editor-row" style="margin: 15px 0">
+							<div class="editor-option text-center">
+								<label class="small">All option</label>
+								<input type="checkbox" ng-model="current.includeAll" ng-checked="current.includeAll" ng-change="runQuery()">
+							</div>
+							<div class="editor-option" ng-show="current.includeAll">
+								<label class="small">All format</label>
+								<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>
+							</div>
+							<div class="editor-option" ng-show="current.includeAll">
+								<label class="small">All value</label>
+								<input type="text" class="input-xlarge" ng-model='current.options[0].value'></input>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="editor-option">
+					<div class="editor-row">
+						<div class="editor-option" >
+							<label class="small">Variable values (showing 20/{{current.options.length}})</label>
+							<ul class="grafana-options-list">
+								<li ng-repeat="option in current.options | limitTo: 20">
+									{{option.text}}
+								</li>
+							</ul>
+						</div>
+					</div>
+				</div>
+			</div>
+
+		</div>
+
+	</div>
+
+	<div class="dashboard-editor-footer">
+		<button type="button" class="btn btn-success pull-left" ng-show="editor.index === 2" ng-click="update();">Update</button>
+		<button type="button" class="btn btn-success pull-left" ng-show="editor.index === 1" ng-click="add();">Add</button>
+		<button type="button" class="btn btn-success pull-right" ng-click="dismiss();">Close</button>
+	</div>
+</div>
+
+<!-- <div class="span4"> -->
+	<!-- 	<tip class="info&#45;box"> -->
+	<!-- 	<i class="icon&#45;question&#45;sign"></i> -->
+	<!-- 	The templating feature in Grafana lets easily create and manage templated queries. Templated queries use [[replacement]] syntax -->
+	<!-- 	to replace a part of your metric key or query. By using this feature you can make your dashboards more generic. You can for example create -->
+	<!-- 	a query replacement for your cluster name or server name. Then use that replacement in your metric queries and change -->
+	<!-- 	it globably for all graphs on the dashboard. -->
+	<!-- 	<br/><br/> -->
+	<!-- 	</tip> -->
+	<!-- </div> -->

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

@@ -34,9 +34,9 @@ function (angular) {
       .then(function(dashboard) {
       .then(function(dashboard) {
         $scope.emitAppEvent('setup-dashboard', dashboard);
         $scope.emitAppEvent('setup-dashboard', dashboard);
       }).then(null, function(error) {
       }).then(null, function(error) {
+        $scope.emitAppEvent('setup-dashboard', { title: 'Grafana'});
         alertSrv.set('Error', error, 'error');
         alertSrv.set('Error', error, 'error');
       });
       });
-
   });
   });
 
 
 });
 });

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

@@ -1,7 +1,9 @@
 define([
 define([
   './alertSrv',
   './alertSrv',
   './datasourceSrv',
   './datasourceSrv',
-  './filterSrv',
+  './timeSrv',
+  './templateSrv',
+  './templateValuesSrv',
   './panelSrv',
   './panelSrv',
   './timer',
   './timer',
   './panelMove',
   './panelMove',

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

@@ -9,7 +9,6 @@ define([
 
 
   module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) {
   module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) {
     var promiseCached;
     var promiseCached;
-    var annotationPanel;
     var list = [];
     var list = [];
     var timezone;
     var timezone;
 
 
@@ -22,9 +21,8 @@ define([
       list = [];
       list = [];
     };
     };
 
 
-    this.getAnnotations = function(filterSrv, rangeUnparsed, dashboard) {
-      annotationPanel = _.findWhere(dashboard.pulldowns, { type: 'annotations' });
-      if (!annotationPanel.enable) {
+    this.getAnnotations = function(rangeUnparsed, dashboard) {
+      if (!dashboard.annotations.enable) {
         return $q.when(null);
         return $q.when(null);
       }
       }
 
 
@@ -33,12 +31,12 @@ define([
       }
       }
 
 
       timezone = dashboard.timezone;
       timezone = dashboard.timezone;
-      var annotations = _.where(annotationPanel.annotations, { enable: true });
+      var annotations = _.where(dashboard.annotations.list, { enable: true });
 
 
       var promises  = _.map(annotations, function(annotation) {
       var promises  = _.map(annotations, function(annotation) {
         var datasource = datasourceSrv.get(annotation.datasource);
         var datasource = datasourceSrv.get(annotation.datasource);
 
 
-        return datasource.annotationQuery(annotation, filterSrv, rangeUnparsed)
+        return datasource.annotationQuery(annotation, rangeUnparsed)
           .then(this.receiveAnnotationResults)
           .then(this.receiveAnnotationResults)
           .then(null, errorHandler);
           .then(null, errorHandler);
       }, this);
       }, this);

+ 5 - 3
src/app/services/dashboard/dashboardKeyBindings.js

@@ -18,11 +18,11 @@ 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(evt) {
-        scope.emitAppEvent('open-search', evt);
+      keyboardManager.bind('ctrl+f', function() {
+        scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('ctrl+h', function() {
       keyboardManager.bind('ctrl+h', function() {
@@ -53,6 +53,8 @@ function(angular, $) {
           modalData.$scope.dismiss();
           modalData.$scope.dismiss();
         }
         }
 
 
+        scope.emitAppEvent('hide-dash-editor');
+
         scope.exitFullscreen();
         scope.exitFullscreen();
       }, { inputDisabled: true });
       }, { inputDisabled: true });
     };
     };

+ 36 - 40
src/app/services/dashboard/dashboardSrv.js

@@ -11,7 +11,7 @@ function (angular, $, kbn, _, moment) {
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.factory('dashboardSrv', function(timer, $rootScope, $timeout) {
+  module.factory('dashboardSrv', function($rootScope)  {
 
 
     function DashboardModel (data) {
     function DashboardModel (data) {
 
 
@@ -25,12 +25,13 @@ function (angular, $, kbn, _, moment) {
       this.tags = data.tags || [];
       this.tags = data.tags || [];
       this.style = data.style || "dark";
       this.style = data.style || "dark";
       this.timezone = data.timezone || 'browser';
       this.timezone = data.timezone || 'browser';
-      this.editable = data.editble || true;
+      this.editable = data.editable || true;
+      this.hideControls = data.hideControls || false;
       this.rows = data.rows || [];
       this.rows = data.rows || [];
-      this.pulldowns = data.pulldowns || [];
       this.nav = data.nav || [];
       this.nav = data.nav || [];
       this.time = data.time || { from: 'now-6h', to: 'now' };
       this.time = data.time || { from: 'now-6h', to: 'now' };
-      this.templating = data.templating || { list: [] };
+      this.templating = data.templating || { list: [], enable: false };
+      this.annotations = data.annotations || { list: [], enable: false};
       this.refresh = data.refresh;
       this.refresh = data.refresh;
       this.version = data.version || 0;
       this.version = data.version || 0;
 
 
@@ -38,14 +39,6 @@ function (angular, $, kbn, _, moment) {
         this.nav.push({ type: 'timepicker' });
         this.nav.push({ type: 'timepicker' });
       }
       }
 
 
-      if (!_.findWhere(this.pulldowns, {type: 'filtering'})) {
-        this.pulldowns.push({ type: 'filtering', enable: false });
-      }
-
-      if (!_.findWhere(this.pulldowns, {type: 'annotations'})) {
-        this.pulldowns.push({ type: 'annotations', enable: false });
-      }
-
       this.updateSchema(data);
       this.updateSchema(data);
     }
     }
 
 
@@ -122,34 +115,13 @@ function (angular, $, kbn, _, moment) {
       $rootScope.$broadcast('refresh');
       $rootScope.$broadcast('refresh');
     };
     };
 
 
-    p.start_scheduled_refresh = function (after_ms) {
-      this.cancel_scheduled_refresh();
-      this.refresh_timer = timer.register($timeout(function () {
-        this.start_scheduled_refresh(after_ms);
-        this.emit_refresh();
-      }.bind(this), after_ms));
-    };
-
-    p.cancel_scheduled_refresh = function () {
-      timer.cancel(this.refresh_timer);
-    };
-
-    p.set_interval = function (interval) {
-      this.refresh = interval;
-      if (interval) {
-        var _i = kbn.interval_to_ms(interval);
-        this.start_scheduled_refresh(_i);
-      } else {
-        this.cancel_scheduled_refresh();
-      }
-    };
-
     p.updateSchema = function(old) {
     p.updateSchema = function(old) {
+      var i, j, k;
       var oldVersion = this.version;
       var oldVersion = this.version;
       var panelUpgrades = [];
       var panelUpgrades = [];
-      this.version = 4;
+      this.version = 6;
 
 
-      if (oldVersion === 4) {
+      if (oldVersion === 6) {
         return;
         return;
       }
       }
 
 
@@ -159,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;
         }
         }
@@ -224,14 +196,38 @@ function (angular, $, kbn, _, moment) {
         });
         });
       }
       }
 
 
+      if (oldVersion < 6) {
+        // move pulldowns to new schema
+        var filtering = _.findWhere(old.pulldowns, { type: 'filtering' });
+        var annotations = _.findWhere(old.pulldowns, { type: 'annotations' });
+        if (filtering) {
+          this.templating.enable = filtering.enable;
+        }
+        if (annotations) {
+          this.annotations = {
+            list: annotations.annotations,
+            enable: annotations.enable
+          };
+        }
+
+        // update template variables
+        for (i = 0 ; i < this.templating.list.length; i++) {
+          var variable = this.templating.list[i];
+          if (variable.datasource === void 0) { variable.datasource = null; }
+          if (variable.type === 'filter') { variable.type = 'query'; }
+          if (variable.type === void 0) { variable.type = 'query'; }
+          if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
+        }
+      }
+
       if (panelUpgrades.length === 0) {
       if (panelUpgrades.length === 0) {
         return;
         return;
       }
       }
 
 
-      for (var i = 0; i < this.rows.length; i++) {
+      for (i = 0; i < this.rows.length; i++) {
         var row = this.rows[i];
         var row = this.rows[i];
-        for (var j = 0; j < row.panels.length; j++) {
-          for (var k = 0; k < panelUpgrades.length; k++) {
+        for (j = 0; j < row.panels.length; j++) {
+          for (k = 0; k < panelUpgrades.length; k++) {
             panelUpgrades[k](row.panels[j]);
             panelUpgrades[k](row.panels[j]);
           }
           }
         }
         }

+ 34 - 27
src/app/services/dashboard/dashboardViewStateSrv.js

@@ -14,15 +14,16 @@ 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() {
-        self.update({ fullscreen: false });
+        if (self.state.fullscreen) {
+          self.update({ fullscreen: false });
+        }
       };
       };
 
 
-      $scope.onAppEvent('dashboard-saved', function() {
-        self.update({ fullscreen: false });
-      });
-
       $scope.onAppEvent('$routeUpdate', function() {
       $scope.onAppEvent('$routeUpdate', function() {
         var urlState = self.getQueryStringState();
         var urlState = self.getQueryStringState();
         if (self.needsSync(urlState)) {
         if (self.needsSync(urlState)) {
@@ -30,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();
@@ -78,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;
       }
       }
@@ -120,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);
@@ -137,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);
       }
       }
 
 

+ 7 - 4
src/app/services/datasourceSrv.js

@@ -13,7 +13,7 @@ function (angular, _, config) {
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('datasourceSrv', function($q, filterSrv, $http, $injector) {
+  module.service('datasourceSrv', function($q, $http, $injector) {
     var datasources = {};
     var datasources = {};
     var metricSources = [];
     var metricSources = [];
     var annotationSources = [];
     var annotationSources = [];
@@ -21,10 +21,12 @@ function (angular, _, config) {
 
 
     this.init = function() {
     this.init = function() {
       _.each(config.datasources, function(value, key) {
       _.each(config.datasources, function(value, key) {
-        datasources[key] = this.datasourceFactory(value);
+        var ds = this.datasourceFactory(value);
         if (value.default) {
         if (value.default) {
-          this.default = datasources[key];
+          this.default = ds;
+          ds.default = true;
         }
         }
+        datasources[key] = ds;
       }, this);
       }, this);
 
 
       if (!this.default) {
       if (!this.default) {
@@ -38,6 +40,7 @@ function (angular, _, config) {
           metricSources.push({
           metricSources.push({
             name: value.name,
             name: value.name,
             value: value.default ? null : key,
             value: value.default ? null : key,
+            default: value.default,
           });
           });
         }
         }
         if (value.supportAnnotations) {
         if (value.supportAnnotations) {
@@ -78,7 +81,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() {

+ 81 - 25
src/app/services/elasticsearch/es-datasource.js

@@ -1,17 +1,16 @@
 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');
 
 
-  module.factory('ElasticDatasource', function($q, $http) {
+  module.factory('ElasticDatasource', function($q, $http, templateSrv) {
 
 
     function ElasticDatasource(datasource) {
     function ElasticDatasource(datasource) {
       this.type = 'elastic';
       this.type = 'elastic';
@@ -60,7 +59,7 @@ function (angular, _, $, config, kbn, moment) {
         });
         });
     };
     };
 
 
-    ElasticDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) {
+    ElasticDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
       var range = {};
       var range = {};
       var timeField = annotation.timeField || '@timestamp';
       var timeField = annotation.timeField || '@timestamp';
       var queryString = annotation.query || '*';
       var queryString = annotation.query || '*';
@@ -73,10 +72,14 @@ function (angular, _, $, config, kbn, moment) {
         to: rangeUnparsed.to,
         to: rangeUnparsed.to,
       };
       };
 
 
-      var queryInterpolated = filterSrv.applyTemplateToTarget(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 = [];
@@ -84,9 +87,16 @@ function (angular, _, $, config, kbn, moment) {
 
 
         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(),
+            time: moment.utc(time).valueOf(),
             title: source[titleField],
             title: source[titleField],
           };
           };
 
 
@@ -108,25 +118,29 @@ function (angular, _, $, config, kbn, moment) {
       });
       });
     };
     };
 
 
+    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 +162,29 @@ 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);
+            return { title: title, url: '/dashboard/db/' + id };
+          }, function() {
+            throw 'Failed to save to elasticsearch';
           });
           });
       }
       }
     };
     };
 
 
+    ElasticDatasource.prototype._removeUnslugifiedDashboard = function(saveResult, title) {
+      if (saveResult.statusText !== 'Created') { 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 +209,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 +235,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 +272,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
             });
             });
           }
           }

+ 0 - 98
src/app/services/filterSrv.js

@@ -1,98 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'config',
-  'kbn'
-], function (angular, _, config, kbn) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.factory('filterSrv', function($rootScope, $timeout, $routeParams) {
-    var result = {
-
-      updateTemplateData: function(initial) {
-        var _templateData = {};
-        _.each(this.templateParameters, function(templateParameter) {
-          if (initial) {
-            var urlValue = $routeParams[ templateParameter.name ];
-            if (urlValue) {
-              templateParameter.current = { text: urlValue, value: urlValue };
-            }
-          }
-          if (!templateParameter.current || !templateParameter.current.value) {
-            return;
-          }
-          _templateData[templateParameter.name] = templateParameter.current.value;
-        });
-        this._templateData = _templateData;
-      },
-
-      addTemplateParameter: function(templateParameter) {
-        this.templateParameters.push(templateParameter);
-        this.updateTemplateData();
-      },
-
-      applyTemplateToTarget: function(target) {
-        if (!target || target.indexOf('[[') === -1) {
-          return target;
-        }
-
-        return _.template(target, this._templateData, this.templateSettings);
-      },
-
-      setTime: function(time) {
-        _.extend(this.time, time);
-
-        // disable refresh if we have an absolute time
-        if (time.to !== 'now') {
-          this.old_refresh = this.dashboard.refresh;
-          this.dashboard.set_interval(false);
-        }
-        else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) {
-          this.dashboard.set_interval(this.old_refresh);
-          this.old_refresh = null;
-        }
-
-        $timeout(this.dashboard.emit_refresh, 0);
-      },
-
-      timeRange: function(parse) {
-        var _t = this.time;
-        if(_.isUndefined(_t) || _.isUndefined(_t.from)) {
-          return false;
-        }
-        if(parse === false) {
-          return {
-            from: _t.from,
-            to: _t.to
-          };
-        } else {
-          var _from = _t.from;
-          var _to = _t.to || new Date();
-
-          return {
-            from : kbn.parseDate(_from),
-            to : kbn.parseDate(_to)
-          };
-        }
-      },
-
-      removeTemplateParameter: function(templateParameter) {
-        this.templateParameters = _.without(this.templateParameters, templateParameter);
-        this.dashboard.templating.list = this.templateParameters;
-      },
-
-      init: function(dashboard) {
-        this.dashboard = dashboard;
-        this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g };
-        this.time = dashboard.time;
-        this.templateParameters = dashboard.templating.list;
-        this.updateTemplateData(true);
-      }
-    };
-
-    return result;
-  });
-
-});

+ 56 - 26
src/app/services/graphite/gfunc.js

@@ -24,6 +24,14 @@ function (_) {
     index[funcDef.shortName || funcDef.name] = funcDef;
     index[funcDef.shortName || funcDef.name] = funcDef;
   }
   }
 
 
+  var optionalSeriesRefArgs = [
+    { name: 'other', type: 'value_or_series', optional: true },
+    { name: 'other', type: 'value_or_series', optional: true },
+    { name: 'other', type: 'value_or_series', optional: true },
+    { name: 'other', type: 'value_or_series', optional: true },
+    { name: 'other', type: 'value_or_series', optional: true }
+  ];
+
   addFuncDef({
   addFuncDef({
     name: 'scaleToSeconds',
     name: 'scaleToSeconds',
     category: categories.Transform,
     category: categories.Transform,
@@ -58,20 +66,40 @@ function (_) {
   });
   });
 
 
   addFuncDef({
   addFuncDef({
-    name: 'sumSeries',
-    shortName: 'sum',
-    category: categories.Combine,
+    name: 'diffSeries',
+    params: optionalSeriesRefArgs,
+    defaultParams: ['#A'],
+    category: categories.Calculate,
   });
   });
 
 
   addFuncDef({
   addFuncDef({
-    name: 'diffSeries',
+    name: 'divideSeries',
+    params: optionalSeriesRefArgs,
+    defaultParams: ['#A'],
+    category: categories.Calculate,
+  });
+
+  addFuncDef({
+    name: 'asPercent',
+    params: optionalSeriesRefArgs,
+    defaultParams: ['#A'],
+    category: categories.Calculate,
+  });
+
+  addFuncDef({
+    name: 'sumSeries',
+    shortName: 'sum',
     category: categories.Combine,
     category: categories.Combine,
+    params: optionalSeriesRefArgs,
+    defaultParams: [''],
   });
   });
 
 
   addFuncDef({
   addFuncDef({
     name: 'averageSeries',
     name: 'averageSeries',
     shortName: 'avg',
     shortName: 'avg',
     category: categories.Combine,
     category: categories.Combine,
+    params: optionalSeriesRefArgs,
+    defaultParams: [''],
   });
   });
 
 
   addFuncDef({
   addFuncDef({
@@ -280,8 +308,8 @@ function (_) {
   addFuncDef({
   addFuncDef({
     name: 'nonNegativeDerivative',
     name: 'nonNegativeDerivative',
     category: categories.Transform,
     category: categories.Transform,
-    params: [{ name: "max value or 0", type: "int", }],
-    defaultParams: [0]
+    params: [{ name: "max value or 0", type: "int", optional: true }],
+    defaultParams: ['']
   });
   });
 
 
   addFuncDef({
   addFuncDef({
@@ -482,23 +510,35 @@ function (_) {
     categories[catName] = _.sortBy(funcList, 'name');
     categories[catName] = _.sortBy(funcList, 'name');
   });
   });
 
 
-  function FuncInstance(funcDef) {
+  function FuncInstance(funcDef, options) {
     this.def = funcDef;
     this.def = funcDef;
-    this.params = funcDef.defaultParams.slice(0);
+    this.params = [];
+
+    if (options && options.withDefaultParams) {
+      this.params = funcDef.defaultParams.slice(0);
+    }
+
     this.updateText();
     this.updateText();
   }
   }
 
 
   FuncInstance.prototype.render = function(metricExp) {
   FuncInstance.prototype.render = function(metricExp) {
     var str = this.def.name + '(';
     var str = this.def.name + '(';
-    var parameters = _.map(this.params, function(value) {
-      return _.isString(value) ? "'" + value + "'" : value;
-    });
+    var parameters = _.map(this.params, function(value, index) {
+
+      var paramType = this.def.params[index].type;
+      if (paramType === 'int' || paramType === 'value_or_series') {
+        return value;
+      }
 
 
-    if (metricExp !== undefined) {
+      return "'" + value + "'";
+
+    }, this);
+
+    if (metricExp) {
       parameters.unshift(metricExp);
       parameters.unshift(metricExp);
     }
     }
 
 
-    return str + parameters.join(',') + ')';
+    return str + parameters.join(', ') + ')';
   };
   };
 
 
   FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
   FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
@@ -522,9 +562,6 @@ function (_) {
     if (strValue === '' && this.def.params[index].optional) {
     if (strValue === '' && this.def.params[index].optional) {
       this.params.splice(index, 1);
       this.params.splice(index, 1);
     }
     }
-    else if (this.def.params[index].type === 'int') {
-      this.params[index] = parseFloat(strValue, 10);
-    }
     else {
     else {
       this.params[index] = strValue;
       this.params[index] = strValue;
     }
     }
@@ -539,27 +576,20 @@ function (_) {
     }
     }
 
 
     var text = this.def.name + '(';
     var text = this.def.name + '(';
-    _.each(this.def.params, function(param, index) {
-      if (param.optional && this.params[index] === undefined) {
-        return;
-      }
-
-      text += this.params[index] + ', ';
-    }, this);
-    text = text.substring(0, text.length - 2);
+    text += this.params.join(', ');
     text += ')';
     text += ')';
     this.text = text;
     this.text = text;
   };
   };
 
 
   return {
   return {
-    createFuncInstance: function(funcDef) {
+    createFuncInstance: function(funcDef, options) {
       if (_.isString(funcDef)) {
       if (_.isString(funcDef)) {
         if (!index[funcDef]) {
         if (!index[funcDef]) {
           throw { message: 'Method not found ' + name };
           throw { message: 'Method not found ' + name };
         }
         }
         funcDef = index[funcDef];
         funcDef = index[funcDef];
       }
       }
-      return new FuncInstance(funcDef);
+      return new FuncInstance(funcDef, options);
     },
     },
 
 
     getFuncDef: function(name) {
     getFuncDef: function(name) {

+ 55 - 24
src/app/services/graphite/graphiteDatasource.js

@@ -11,7 +11,7 @@ function (angular, _, $, config, kbn, moment) {
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.factory('GraphiteDatasource', function($q, $http) {
+  module.factory('GraphiteDatasource', function($q, $http, templateSrv) {
 
 
     function GraphiteDatasource(datasource) {
     function GraphiteDatasource(datasource) {
       this.type = 'graphite';
       this.type = 'graphite';
@@ -26,7 +26,7 @@ function (angular, _, $, config, kbn, moment) {
       this.cacheTimeout = datasource.cacheTimeout;
       this.cacheTimeout = datasource.cacheTimeout;
     }
     }
 
 
-    GraphiteDatasource.prototype.query = function(filterSrv, options) {
+    GraphiteDatasource.prototype.query = function(options) {
       try {
       try {
         var graphOptions = {
         var graphOptions = {
           from: this.translateTime(options.range.from, 'round-down'),
           from: this.translateTime(options.range.from, 'round-down'),
@@ -37,7 +37,7 @@ function (angular, _, $, config, kbn, moment) {
           maxDataPoints: options.maxDataPoints,
           maxDataPoints: options.maxDataPoints,
         };
         };
 
 
-        var params = this.buildGraphiteParams(filterSrv, graphOptions);
+        var params = this.buildGraphiteParams(graphOptions);
 
 
         if (options.format === 'png') {
         if (options.format === 'png') {
           return $q.when(this.url + '/render' + '?' + params.join('&'));
           return $q.when(this.url + '/render' + '?' + params.join('&'));
@@ -60,10 +60,10 @@ function (angular, _, $, config, kbn, moment) {
       }
       }
     };
     };
 
 
-    GraphiteDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) {
+    GraphiteDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
       // Graphite metric as annotation
       // Graphite metric as annotation
       if (annotation.target) {
       if (annotation.target) {
-        var target = filterSrv.applyTemplateToTarget(annotation.target);
+        var target = templateSrv.replace(annotation.target);
         var graphiteQuery = {
         var graphiteQuery = {
           range: rangeUnparsed,
           range: rangeUnparsed,
           targets: [{ target: target }],
           targets: [{ target: target }],
@@ -71,7 +71,7 @@ function (angular, _, $, config, kbn, moment) {
           maxDataPoints: 100
           maxDataPoints: 100
         };
         };
 
 
-        return this.query(filterSrv, graphiteQuery)
+        return this.query(graphiteQuery)
           .then(function(result) {
           .then(function(result) {
             var list = [];
             var list = [];
 
 
@@ -95,7 +95,7 @@ function (angular, _, $, config, kbn, moment) {
       }
       }
       // Graphite event as annotation
       // Graphite event as annotation
       else {
       else {
-        var tags = filterSrv.applyTemplateToTarget(annotation.tags);
+        var tags = templateSrv.replace(annotation.tags);
         return this.events({ range: rangeUnparsed, tags: tags })
         return this.events({ range: rangeUnparsed, tags: tags })
           .then(function(results) {
           .then(function(results) {
             var list = [];
             var list = [];
@@ -166,10 +166,10 @@ function (angular, _, $, config, kbn, moment) {
       return date.unix();
       return date.unix();
     };
     };
 
 
-    GraphiteDatasource.prototype.metricFindQuery = function(filterSrv, query) {
+    GraphiteDatasource.prototype.metricFindQuery = function(query) {
       var interpolated;
       var interpolated;
       try {
       try {
-        interpolated = encodeURIComponent(filterSrv.applyTemplateToTarget(query));
+        interpolated = encodeURIComponent(templateSrv.replace(query));
       }
       }
       catch(err) {
       catch(err) {
         return $q.reject(err);
         return $q.reject(err);
@@ -210,31 +210,62 @@ function (angular, _, $, config, kbn, moment) {
       return $http(options);
       return $http(options);
     };
     };
 
 
-    GraphiteDatasource.prototype.buildGraphiteParams = function(filterSrv, options) {
-      var clean_options = [];
-      var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+    GraphiteDatasource.prototype._seriesRefLetters = [
+      '#A', '#B', '#C', '#D',
+      '#E', '#F', '#G', '#H',
+      '#I', '#J', '#K', '#L',
+      '#M', '#N', '#O'
+    ];
+
+    GraphiteDatasource.prototype.buildGraphiteParams = function(options) {
+      var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+      var clean_options = [], targets = {};
+      var target, targetValue, i;
+      var regex = /(\#[A-Z])/g;
+      var intervalFormatFixRegex = /'(\d+)m'/gi;
 
 
       if (options.format !== 'png') {
       if (options.format !== 'png') {
         options['format'] = 'json';
         options['format'] = 'json';
       }
       }
 
 
-      _.each(options, function (value, key) {
-        if ($.inArray(key, graphite_options) === -1) {
-          return;
+      function fixIntervalFormat(match) {
+        return match.replace('m', 'min').replace('M', 'mon');
+      }
+
+      for (i = 0; i < options.targets.length; i++) {
+        target = options.targets[i];
+        if (!target.target) {
+          continue;
         }
         }
 
 
-        if (key === "targets") {
-          _.each(value, function (value) {
-            if (value.target && !value.hide) {
-              var targetValue = filterSrv.applyTemplateToTarget(value.target);
-              clean_options.push("target=" + encodeURIComponent(targetValue));
-            }
-          }, this);
+        targetValue = templateSrv.replace(target.target);
+        targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
+        targets[this._seriesRefLetters[i]] = targetValue;
+      }
+
+      function nestedSeriesRegexReplacer(match) {
+        return targets[match];
+      }
+
+      for (i = 0; i < options.targets.length; i++) {
+        target = options.targets[i];
+        if (!target.target || target.hide) {
+          continue;
         }
         }
-        else if (value) {
+
+        targetValue = targets[this._seriesRefLetters[i]];
+        targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
+
+        clean_options.push("target=" + encodeURIComponent(targetValue));
+      }
+
+      _.each(options, function (value, key) {
+        if ($.inArray(key, graphite_options) === -1) { return; }
+        if (value) {
           clean_options.push(key + "=" + encodeURIComponent(value));
           clean_options.push(key + "=" + encodeURIComponent(value));
         }
         }
-      }, this);
+      });
+
       return clean_options;
       return clean_options;
     };
     };
 
 

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

@@ -128,6 +128,7 @@ define([
       i === 93 ||           // templateEnd ]
       i === 93 ||           // templateEnd ]
       i === 63 ||           // ?
       i === 63 ||           // ?
       i === 37 ||           // %
       i === 37 ||           // %
+      i === 35 ||           // #
       i >= 97 && i <= 122;  // a-z
       i >= 97 && i <= 122;  // a-z
   }
   }
 
 

+ 19 - 0
src/app/services/graphite/parser.js

@@ -157,6 +157,7 @@ define([
       var param =
       var param =
         this.functionCall() ||
         this.functionCall() ||
         this.numericLiteral() ||
         this.numericLiteral() ||
+        this.seriesRefExpression() ||
         this.metricExpression() ||
         this.metricExpression() ||
         this.stringLiteral();
         this.stringLiteral();
 
 
@@ -168,6 +169,24 @@ define([
       return [param].concat(this.functionParameters());
       return [param].concat(this.functionParameters());
     },
     },
 
 
+    seriesRefExpression: function() {
+      if (!this.match('identifier')) {
+        return null;
+      }
+
+      var value = this.tokens[this.index].value;
+      if (!value.match(/\#[A-Z]/)) {
+        return null;
+      }
+
+      var token = this.consumeToken();
+
+      return {
+        type: 'series-ref',
+        value: token.value
+      };
+    },
+
     numericLiteral: function () {
     numericLiteral: function () {
       if (!this.match('number')) {
       if (!this.match('number')) {
         return null;
         return null;

+ 67 - 0
src/app/services/influxdb/influxQueryBuilder.js

@@ -0,0 +1,67 @@
+define([
+],
+function () {
+  'use strict';
+
+  function InfluxQueryBuilder(target) {
+    this.target = target;
+  }
+
+  var p = InfluxQueryBuilder.prototype;
+
+  p.build = function() {
+    return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery();
+  };
+
+  p._buildQuery = function() {
+    var target = this.target;
+    var query = 'select ';
+    var seriesName = target.series;
+
+    if(!seriesName.match('^/.*/')) {
+      seriesName = '"' + seriesName+ '"';
+    }
+
+    if (target.groupby_field) {
+      query += target.groupby_field + ', ';
+    }
+
+    query +=  target.function + '(' + target.column + ')';
+    query += ' from ' + seriesName + ' where $timeFilter';
+
+    if (target.condition) {
+      query += ' and ' + target.condition;
+    }
+
+    query += ' group by time($interval)';
+
+    if (target.groupby_field) {
+      query += ', ' + target.groupby_field;
+      this.groupByField = target.groupby_field;
+    }
+
+    if (target.fill) {
+      query += ' fill(' + target.fill + ')';
+    }
+
+    query += " order asc";
+    target.query = query;
+
+    return query;
+  };
+
+  p._modifyRawQuery = function () {
+    var query = this.target.query.replace(";", "");
+
+    var queryElements = query.split(" ");
+    var lowerCaseQueryElements = query.toLowerCase().split(" ");
+
+    if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
+      this.groupByField = lowerCaseQueryElements[1].replace(',', '');
+    }
+
+    return queryElements.join(" ");
+  };
+
+  return InfluxQueryBuilder;
+});

+ 87 - 102
src/app/services/influxdb/influxdbDatasource.js

@@ -2,14 +2,15 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'kbn',
   'kbn',
-  './influxSeries'
+  './influxSeries',
+  './influxQueryBuilder'
 ],
 ],
-function (angular, _, kbn, InfluxSeries) {
+function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.factory('InfluxDatasource', function($q, $http) {
+  module.factory('InfluxDatasource', function($q, $http, templateSrv) {
 
 
     function InfluxDatasource(datasource) {
     function InfluxDatasource(datasource) {
       this.type = 'influxDB';
       this.type = 'influxDB';
@@ -18,9 +19,7 @@ function (angular, _, kbn, InfluxSeries) {
       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.templateSettings = {
-        interpolate : /\[\[([\s\S]+?)\]\]/g,
-      };
+      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;
@@ -31,89 +30,28 @@ function (angular, _, kbn, InfluxSeries) {
       this.annotationEditorSrc = 'app/partials/influxdb/annotation_editor.html';
       this.annotationEditorSrc = 'app/partials/influxdb/annotation_editor.html';
     }
     }
 
 
-    InfluxDatasource.prototype.query = function(filterSrv, options) {
-      var promises = _.map(options.targets, function(target) {
-        var query;
-        var alias = '';
+    InfluxDatasource.prototype.query = function(options) {
+      var timeFilter = getTimeFilter(options);
 
 
+      var promises = _.map(options.targets, function(target) {
         if (target.hide || !((target.series && target.column) || target.query)) {
         if (target.hide || !((target.series && target.column) || target.query)) {
           return [];
           return [];
         }
         }
 
 
-        var timeFilter = getTimeFilter(options);
-        var groupByField;
+        // build query
+        var queryBuilder = new InfluxQueryBuilder(target);
+        var query = queryBuilder.build();
 
 
-        if (target.rawQuery) {
-          query = target.query;
-          query = query.replace(";", "");
-          var queryElements = query.split(" ");
-          var lowerCaseQueryElements = query.toLowerCase().split(" ");
-          var whereIndex = lowerCaseQueryElements.indexOf("where");
-          var groupByIndex = lowerCaseQueryElements.indexOf("group");
-          var orderIndex = lowerCaseQueryElements.indexOf("order");
+        // replace grafana variables
+        query = query.replace('$timeFilter', timeFilter);
+        query = query.replace('$interval', (target.interval || options.interval));
 
 
-          if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
-            groupByField = lowerCaseQueryElements[1].replace(',', '');
-          }
+        // replace templated variables
+        query = templateSrv.replace(query);
 
 
-          if (whereIndex !== -1) {
-            queryElements.splice(whereIndex + 1, 0, timeFilter, "and");
-          }
-          else {
-            if (groupByIndex !== -1) {
-              queryElements.splice(groupByIndex, 0, "where", timeFilter);
-            }
-            else if (orderIndex !== -1) {
-              queryElements.splice(orderIndex, 0, "where", timeFilter);
-            }
-            else {
-              queryElements.push("where");
-              queryElements.push(timeFilter);
-            }
-          }
+        var alias = target.alias ? templateSrv.replace(target.alias) : '';
 
 
-          query = queryElements.join(" ");
-          query = filterSrv.applyTemplateToTarget(query);
-        }
-        else {
-
-          var template = "select [[group]][[group_comma]] [[func]]([[column]]) from [[series]] " +
-                         "where  [[timeFilter]] [[condition_add]] [[condition_key]] [[condition_op]] [[condition_value]] " +
-                         "group by time([[interval]])[[group_comma]] [[group]] order asc";
-
-          var templateData = {
-            series: target.series,
-            column: target.column,
-            func: target.function,
-            timeFilter: timeFilter,
-            interval: target.interval || options.interval,
-            condition_add: target.condition_filter ? 'and' : '',
-            condition_key: target.condition_filter ? target.condition_key : '',
-            condition_op: target.condition_filter ? target.condition_op : '',
-            condition_value: target.condition_filter ? target.condition_value : '',
-            group_comma: target.groupby_field_add && target.groupby_field ? ',' : '',
-            group: target.groupby_field_add ? target.groupby_field : '',
-          };
-
-          if(!templateData.series.match('^/.*/')) {
-            templateData.series = '"' + templateData.series + '"';
-          }
-
-          query = _.template(template, templateData, this.templateSettings);
-          query = filterSrv.applyTemplateToTarget(query);
-
-          if (target.groupby_field_add) {
-            groupByField = target.groupby_field;
-          }
-
-          target.query = query;
-        }
-
-        if (target.alias) {
-          alias = filterSrv.applyTemplateToTarget(target.alias);
-        }
-
-        var handleResponse = _.partial(handleInfluxQueryResponse, alias, groupByField);
+        var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
         return this._seriesQuery(query).then(handleResponse);
         return this._seriesQuery(query).then(handleResponse);
 
 
       }, this);
       }, this);
@@ -121,20 +59,25 @@ function (angular, _, kbn, InfluxSeries) {
       return $q.all(promises).then(function(results) {
       return $q.all(promises).then(function(results) {
         return { data: _.flatten(results) };
         return { data: _.flatten(results) };
       });
       });
-
     };
     };
 
 
-    InfluxDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) {
+    InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
       var timeFilter = getTimeFilter({ range: rangeUnparsed });
       var timeFilter = getTimeFilter({ range: rangeUnparsed });
-      var query = _.template(annotation.query, { timeFilter: timeFilter }, this.templateSettings);
+      var query = annotation.query.replace('$timeFilter', timeFilter);
+      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();
       });
       });
     };
     };
+
     InfluxDatasource.prototype.listColumns = function(seriesName) {
     InfluxDatasource.prototype.listColumns = function(seriesName) {
+      var interpolated = templateSrv.replace(seriesName);
+      if (interpolated[0] !== '/') {
+        interpolated = '/' + interpolated + '/';
+      }
 
 
-      return this._seriesQuery('select * from /' + seriesName + '/ limit 1').then(function(data) {
+      return this._seriesQuery('select * from ' + interpolated + ' limit 1').then(function(data) {
         if (!data) {
         if (!data) {
           return [];
           return [];
         }
         }
@@ -161,10 +104,10 @@ function (angular, _, kbn, InfluxSeries) {
       });
       });
     };
     };
 
 
-    InfluxDatasource.prototype.metricFindQuery = function (filterSrv, query) {
+    InfluxDatasource.prototype.metricFindQuery = function (query) {
       var interpolated;
       var interpolated;
       try {
       try {
-        interpolated = filterSrv.applyTemplateToTarget(query);
+        interpolated = templateSrv.replace(query);
       }
       }
       catch (err) {
       catch (err) {
         return $q.reject(err);
         return $q.reject(err);
@@ -228,6 +171,11 @@ function (angular, _, kbn, InfluxSeries) {
           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);
         });
         });
@@ -240,34 +188,46 @@ function (angular, _, kbn, InfluxSeries) {
       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;
@@ -294,7 +254,7 @@ function (angular, _, kbn, InfluxSeries) {
       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) {
@@ -303,15 +263,34 @@ function (angular, _, kbn, InfluxSeries) {
 
 
       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;
       });
       });
     };
     };
 
 
@@ -322,12 +301,12 @@ function (angular, _, kbn, InfluxSeries) {
         }
         }
         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) {
@@ -352,15 +331,21 @@ function (angular, _, kbn, InfluxSeries) {
           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));

+ 8 - 2
src/app/services/panelMove.js

@@ -69,8 +69,14 @@ function (angular, _) {
     };
     };
 
 
     return {
     return {
-      create: function(dashboard) {
-        return new PanelMoveSrv(dashboard);
+      init: function(dashboard, scope) {
+        var panelMove = new PanelMoveSrv(dashboard);
+
+        scope.panelMoveDrop = panelMove.onDrop;
+        scope.panelMoveStart = panelMove.onStart;
+        scope.panelMoveStop = panelMove.onStop;
+        scope.panelMoveOver = panelMove.onOver;
+        scope.panelMoveOut = panelMove.onOut;
       }
       }
     };
     };
 
 

+ 15 - 13
src/app/services/panelSrv.js

@@ -9,9 +9,8 @@ function (angular, _) {
   module.service('panelSrv', function($rootScope, $timeout, datasourceSrv) {
   module.service('panelSrv', function($rootScope, $timeout, datasourceSrv) {
 
 
     this.init = function($scope) {
     this.init = function($scope) {
-      if (!$scope.panel.span) {
-        $scope.panel.span = 12;
-      }
+      if (!$scope.panel.span) { $scope.panel.span = 12; }
+      if (!$scope.panel.title) { $scope.panel.title = 'No title'; }
 
 
       var menu = [
       var menu = [
         {
         {
@@ -52,6 +51,13 @@ function (angular, _) {
           ],
           ],
           condition: true
           condition: true
         },
         },
+        {
+          text: 'Advanced',
+          submenu: [
+            { text: 'Panel JSON', click: 'editPanelJson()' },
+          ],
+          condition: true
+        },
         {
         {
           text: 'Remove',
           text: 'Remove',
           click: 'remove_panel_from_row(row, panel)',
           click: 'remove_panel_from_row(row, panel)',
@@ -62,6 +68,10 @@ function (angular, _) {
       $scope.inspector = {};
       $scope.inspector = {};
       $scope.panelMeta.menu = _.where(menu, { condition: true });
       $scope.panelMeta.menu = _.where(menu, { condition: true });
 
 
+      $scope.editPanelJson = function() {
+        $scope.emitAppEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
+      };
+
       $scope.updateColumnSpan = function(span) {
       $scope.updateColumnSpan = function(span) {
         $scope.panel.span = span;
         $scope.panel.span = span;
 
 
@@ -84,7 +94,7 @@ function (angular, _) {
         $scope.datasource = datasourceSrv.get(datasource);
         $scope.datasource = datasourceSrv.get(datasource);
 
 
         if (!$scope.datasource) {
         if (!$scope.datasource) {
-          $scope.panel.error = "Cannot find datasource " + datasource;
+          $scope.panelMeta.error = "Cannot find datasource " + datasource;
           return;
           return;
         }
         }
       };
       };
@@ -111,7 +121,6 @@ function (angular, _) {
 
 
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.setDatasource($scope.panel.datasource);
       $scope.setDatasource($scope.panel.datasource);
-
       $scope.dashboardViewState.registerPanel($scope);
       $scope.dashboardViewState.registerPanel($scope);
 
 
       if ($scope.get_data) {
       if ($scope.get_data) {
@@ -119,7 +128,7 @@ function (angular, _) {
         $scope.get_data = function() {
         $scope.get_data = function() {
           if ($scope.otherPanelInFullscreenMode()) { return; }
           if ($scope.otherPanelInFullscreenMode()) { return; }
 
 
-          delete $scope.panel.error;
+          delete $scope.panelMeta.error;
           $scope.panelMeta.loading = true;
           $scope.panelMeta.loading = true;
 
 
           panel_get_data();
           panel_get_data();
@@ -129,13 +138,6 @@ function (angular, _) {
           $scope.get_data();
           $scope.get_data();
         }
         }
       }
       }
-
-      if ($rootScope.profilingEnabled) {
-        $rootScope.performance.panelsInitialized++;
-        if ($rootScope.performance.panelsInitialized === $scope.dashboard.rows.length) {
-          $rootScope.performance.allPanelsInitialized = new Date().getTime();
-        }
-      }
     };
     };
   });
   });
 
 

+ 93 - 0
src/app/services/templateSrv.js

@@ -0,0 +1,93 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('templateSrv', function() {
+    var self = this;
+
+    this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
+    this._values = {};
+    this._texts = {};
+    this._grafanaVariables = {};
+
+    this.init = function(variables) {
+      this.variables = variables;
+      this.updateTemplateData();
+    };
+
+    this.updateTemplateData = function() {
+      this._values = {};
+      this._texts = {};
+
+      _.each(this.variables, function(variable) {
+        if (!variable.current || !variable.current.value) { return; }
+
+        this._values[variable.name] = variable.current.value;
+        this._texts[variable.name] = variable.current.text;
+      }, this);
+    };
+
+    this.setGrafanaVariable = function (name, value) {
+      this._grafanaVariables[name] = value;
+    };
+
+    this.variableExists = function(expression) {
+      this._regex.lastIndex = 0;
+      var match = this._regex.exec(expression);
+      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) {
+      if (!str || !_.isString(str)) { return str; }
+
+      this._regex.lastIndex = 0;
+      return str.replace(this._regex, function(match, g1, g2) {
+        if (self._values[g1 || g2]) {
+          return '<span class="template-variable">' + match + '</span>';
+        }
+        return match;
+      });
+    };
+
+    this.replace = function(target) {
+      if (!target) { return; }
+
+      var value;
+      this._regex.lastIndex = 0;
+
+      return target.replace(this._regex, function(match, g1, g2) {
+        value = self._values[g1 || g2];
+        if (!value) { return match; }
+
+        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;
+      });
+    };
+
+  });
+
+});

+ 171 - 0
src/app/services/templateValuesSrv.js

@@ -0,0 +1,171 @@
+define([
+  'angular',
+  'lodash',
+  'kbn',
+],
+function (angular, _, kbn) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('templateValuesSrv', function($q, $rootScope, datasourceSrv, $routeParams, templateSrv, timeSrv) {
+    var self = this;
+
+    $rootScope.onAppEvent('time-range-changed', function()  {
+      var variable = _.findWhere(self.variables, { type: 'interval' });
+      if (variable) {
+        self.updateAutoInterval(variable);
+      }
+    });
+
+    this.init = function(dashboard, viewstate) {
+      this.variables = dashboard.templating.list;
+      this.viewstate = viewstate;
+      templateSrv.init(this.variables);
+
+      for (var i = 0; i < this.variables.length; i++) {
+        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 (variable.refresh) {
+          this.updateOptions(variable);
+        }
+        else if (variable.type === 'interval') {
+          this.updateAutoInterval(variable);
+        }
+      }
+    };
+
+    this.updateAutoInterval = function(variable) {
+      if (!variable.auto) { return; }
+
+      // add auto option if missing
+      if (variable.options[0].text !== 'auto') {
+        variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
+      }
+
+      var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count);
+      templateSrv.setGrafanaVariable('$__auto_interval', interval);
+    };
+
+    this.setVariableValue = function(variable, option, recursive) {
+      variable.current = option;
+
+      templateSrv.updateTemplateData();
+
+      return this.updateOptionsInChildVariables(variable)
+        .then(function() {
+          if (!recursive) {
+            $rootScope.$broadcast('refresh');
+          }
+        });
+    };
+
+    this.updateOptionsInChildVariables = function(updatedVariable) {
+      var promises = _.map(self.variables, function(otherVariable) {
+        if (otherVariable === updatedVariable) {
+          return;
+        }
+        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
+          return self.updateOptions(otherVariable);
+        }
+      });
+
+      return $q.all(promises);
+    };
+
+    this._updateNonQueryVariable = function(variable) {
+      // extract options in comma seperated string
+      variable.options = _.map(variable.query.split(/[\s,]+/), function(text) {
+        return { text: text, value: text };
+      });
+
+      if (variable.type === 'interval') {
+        self.updateAutoInterval(variable);
+      }
+    };
+
+    this.updateOptions = function(variable) {
+      if (variable.type !== 'query') {
+        self._updateNonQueryVariable(variable);
+        self.setVariableValue(variable, variable.options[0]);
+        return $q.when([]);
+      }
+
+      var datasource = datasourceSrv.get(variable.datasource);
+      return datasource.metricFindQuery(variable.query)
+        .then(function (results) {
+          variable.options = self.metricNamesToVariableValues(variable, results);
+
+          if (variable.includeAll) {
+            self.addAllOption(variable);
+          }
+
+          // if parameter has current value
+          // if it exists in options array keep value
+          if (variable.current) {
+            var currentOption = _.findWhere(variable.options, { text: variable.current.text });
+            if (currentOption) {
+              return self.setVariableValue(variable, currentOption, true);
+            }
+          }
+
+          return self.setVariableValue(variable, variable.options[0], true);
+        });
+    };
+
+    this.metricNamesToVariableValues = function(variable, metricNames) {
+      var regex, options, i, matches;
+      options = {}; // use object hash to remove duplicates
+
+      if (variable.regex) {
+        regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
+      }
+
+      for (i = 0; i < metricNames.length; i++) {
+        var value = metricNames[i].text;
+
+        if (regex) {
+          matches = regex.exec(value);
+          if (!matches) { continue; }
+          if (matches.length > 1) {
+            value = matches[1];
+          }
+        }
+
+        options[value] = value;
+      }
+
+      return _.map(_.keys(options), function(key) {
+        return { text: key, value: key };
+      });
+    };
+
+    this.addAllOption = function(variable) {
+      var allValue = '';
+      switch(variable.allFormat) {
+      case 'wildcard':
+        allValue = '*';
+        break;
+      case 'regex wildcard':
+        allValue = '.*';
+        break;
+      case 'regex values':
+        allValue = '(' + _.pluck(variable.options, 'text').join('|') + ')';
+        break;
+      default:
+        allValue = '{';
+        allValue += _.pluck(variable.options, 'text').join(',');
+        allValue += '}';
+      }
+
+      variable.options.unshift({text: 'All', value: allValue});
+    };
+
+  });
+
+});

+ 121 - 0
src/app/services/timeSrv.js

@@ -0,0 +1,121 @@
+define([
+  'angular',
+  'lodash',
+  'config',
+  'kbn',
+  'moment'
+], function (angular, _, config, kbn, moment) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
+    var self = this;
+
+    this.init = function(dashboard) {
+      timer.cancel_all();
+
+      this.dashboard = dashboard;
+      this.time = dashboard.time;
+
+      this._initTimeFromUrl();
+
+      if(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.dashboard.refresh = interval;
+      if (interval) {
+        var _i = kbn.interval_to_ms(interval);
+        this.start_scheduled_refresh(_i);
+      } else {
+        this.cancel_scheduled_refresh();
+      }
+    };
+
+    this.refreshDashboard = function() {
+      $rootScope.$broadcast('refresh');
+    };
+
+    this.start_scheduled_refresh = function (after_ms) {
+      self.cancel_scheduled_refresh();
+      self.refresh_timer = timer.register($timeout(function () {
+        self.start_scheduled_refresh(after_ms);
+        self.refreshDashboard();
+      }, after_ms));
+    };
+
+    this.cancel_scheduled_refresh = function () {
+      timer.cancel(this.refresh_timer);
+    };
+
+    this.setTime = function(time) {
+      _.extend(this.time, time);
+
+      // disable refresh if we have an absolute time
+      if (time.to !== 'now') {
+        this.old_refresh = this.dashboard.refresh;
+        this.set_interval(false);
+      }
+      else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) {
+        this.set_interval(this.old_refresh);
+        this.old_refresh = null;
+      }
+
+      $rootScope.emitAppEvent('time-range-changed', this.time);
+      $timeout(this.refreshDashboard, 0);
+    };
+
+    this.timeRange = function(parse) {
+      var _t = this.time;
+      if(_.isUndefined(_t) || _.isUndefined(_t.from)) {
+        return false;
+      }
+      if(parse === false) {
+        return {
+          from: _t.from,
+          to: _t.to
+        };
+      } else {
+        var _from = _t.from;
+        var _to = _t.to || new Date();
+
+        return {
+          from: kbn.parseDate(_from),
+          to: kbn.parseDate(_to)
+        };
+      }
+    };
+
+  });
+
+});

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

@@ -81,9 +81,16 @@ function(angular, _, config) {
 
 
       // ignore timespan changes
       // ignore timespan changes
       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' });
 
 

+ 4 - 1
src/config.sample.js

@@ -78,7 +78,7 @@ function (Settings) {
       max_results: 20
       max_results: 20
     },
     },
 
 
-    // default start dashboard
+    // default home dashboard
     default_route: '/dashboard/file/default.json',
     default_route: '/dashboard/file/default.json',
 
 
     // set to false to disable unsaved changes warning
     // set to false to disable unsaved changes warning
@@ -94,6 +94,9 @@ function (Settings) {
       password: ''
       password: ''
     },
     },
 
 
+    // Change window title prefix from 'Grafana - <dashboard title>'
+    window_title_prefix: 'Grafana - ',
+
     // Add your own custom pannels
     // Add your own custom pannels
     plugins: {
     plugins: {
       // list of plugin panels
       // list of plugin panels

+ 10 - 9
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;
@@ -212,7 +212,7 @@ div.subnav {
 
 
 .nav-tabs {
 .nav-tabs {
 
 
-	border-bottom: 1px solid @grayDark;
+	border-bottom: 1px solid @fullEditBorder;
 
 
 	& > li > a {
 	& > li > a {
 		.border-radius(0);
 		.border-radius(0);
@@ -221,8 +221,9 @@ div.subnav {
 	li > a:hover,
 	li > a:hover,
 	li.active > a,
 	li.active > a,
 	li.active > a:hover {
 	li.active > a:hover {
-		border-color: transparent;
-		background-color: @blue;
+	  border-color: transparent;
+	  background-color: transparent;
+		border-bottom: 2px solid @blue;
 		color: @white;
 		color: @white;
 	}
 	}
 
 
@@ -362,7 +363,7 @@ div.subnav {
 	background-image: none;
 	background-image: none;
 	.box-shadow(none);
 	.box-shadow(none);
 	border: none;
 	border: none;
-	.border-radius(0);
+	.border-radius(2px);
 	text-shadow: none;
 	text-shadow: none;
 
 
 	&.disabled {
 	&.disabled {
@@ -544,16 +545,16 @@ a:hover {
 .modal {
 .modal {
  	.border-radius(1px);
  	.border-radius(1px);
 	border-top: solid 1px lighten(@grayDark, 5%);
 	border-top: solid 1px lighten(@grayDark, 5%);
-	background-color: @grayDark;
+	background-color: @grafanaPanelBackground;
 }
 }
 
 
 .modal-header {
 .modal-header {
-	border-bottom: 1px solid @grayDark;
+	border-bottom: 1px solid @grafanaPanelBackground;
 }
 }
 
 
 .modal-footer {
 .modal-footer {
-	background-color: @grayDark;
-	border-top: 1px solid @grayDark;
+	background-color: @grafanaPanelBackground;
+	border-top: 1px solid @grafanaPanelBackground;
 	.border-radius(0 0 0px 0px);
 	.border-radius(0 0 0px 0px);
 	.box-shadow(none);
 	.box-shadow(none);
 }
 }

+ 3 - 2
src/css/less/bootswatch.light.less

@@ -159,8 +159,9 @@ div.subnav {
 	li.active > a,
 	li.active > a,
 	li.active > a:hover {
 	li.active > a:hover {
 		border-color: transparent;
 		border-color: transparent;
-		background-color: @blue;
-		color: @white;
+	  background-color: transparent;
+		border-bottom: 2px solid @blue;
+		color: @blue
 	}
 	}
 
 
 	li.disabled > a {
 	li.disabled > a {

+ 0 - 2
src/css/less/console.less

@@ -8,8 +8,6 @@
 }
 }
 
 
 .grafana-console-header {
 .grafana-console-header {
-  background: @fullEditTabsBackground;
-  border-top: @fullEditTabsBorder;
   padding: 2px 5px;
   padding: 2px 5px;
 }
 }
 
 

+ 125 - 140
src/css/less/grafana.less

@@ -1,8 +1,11 @@
+@import "p_pro.less";
 @import "submenu.less";
 @import "submenu.less";
 @import "graph.less";
 @import "graph.less";
 @import "console.less";
 @import "console.less";
 @import "bootstrap-tagsinput.less";
 @import "bootstrap-tagsinput.less";
-@import "p_pro.less";
+@import "tables_lists.less";
+@import "search.less";
+@import "panel.less";
 
 
 .hide-controls {
 .hide-controls {
   padding: 0;
   padding: 0;
@@ -33,79 +36,17 @@
   }
   }
 }
 }
 
 
-// Search
-
-.grafana-search-panel {
-  .search-field-wrapper {
-    padding: 6px 10px;
-    input {
-      width: 100%;
-    }
-    button {
-      margin: 0 2px 0 0;
-    }
-    > span {
-      display: block;
-      overflow: hidden;
-      padding-right: 25px;
-    }
-  }
-}
-
-.search-results-container {
-  max-height: 600px;
-  overflow: auto;
+.logo-icon {
+  width: 24px;
+  padding: 13px 11px 0 0;
   display: block;
   display: block;
-  .search-result-item a {
-  }
-
-  .search-result-item:hover, .search-result-item.selected {
-    .search-result-link, .icon {
-      color: @grafanaListHighlight;
-    }
-    .search-result-link .label {
-      background-color: @blue;
-    }
-  }
-
-
-  .search-result-link {
-    color: @grafanaListMainLinkColor;
-    .icon {
-      padding-right: 10px;
-      color: @grafanaListHighlightContrast;
-    }
-  }
-
-  .search-result-item:nth-child(odd) {
-    background-color: @grafanaListAccent;
-  }
-
-  .search-result-item {
-    padding: 6px 10px;
-    white-space: nowrap;
-    border-top: 1px solid @grafanaListBorderTop;
-    border-bottom: 1px solid @grafanaListBorderBottom;
-  }
-
-  .search-result-tags {
-    float: right;
-  }
-
-  .search-result-actions {
-    float: right;
-    padding-left: 10px;
-  }
+  float: left;
 }
 }
 
 
-.search-tagview-switch {
-  position: absolute;
-  top: 15px;
-  right: 266px;
-  color: darken(@linkColor, 30%);
-  &.active {
-    color: @linkColor;
-  }
+.page-title {
+ padding: 15px 0;
+ display: block;
+ float: left;
 }
 }
 
 
 .row-button {
 .row-button {
@@ -160,7 +101,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;
@@ -176,49 +117,16 @@
   }
   }
 }
 }
 
 
-.dashboard-fullscreen .container-fluid.main {
-  height: 0px;
-  width: 0px;
-  position: fixed;
-  right: -10000px;
+.dashboard-fullscreen {
+  .row-control-inner {
+    display: none;
+  }
 }
 }
 
 
 .histogram-chart {
 .histogram-chart {
   position:relative;
   position:relative;
 }
 }
 
 
-.panel-full-edit-tabs {
-  margin-top: 30px;
-  min-height: 250px;
-  margin-left: -10px;
-  margin-right: -10px;
-  background-color: @fullEditBackground;
-  border-top: 1px solid @fullEditBorder;
-
-  .tabs {
-    .nav-tabs {
-      margin: 0;
-      background: @fullEditTabsBackground;
-      border-top: 1px solid @fullEditTabsBorder;
-    }
-
-    .tab-content {
-      display: none;
-    }
-  }
-  .tab-content {
-    overflow: visible;
-    padding: 15px;
-  }
-
-  .nav-tabs > li > a {
-    line-height: 15px;
-    padding-top: 6px;
-    padding-bottom: 6px;
-    font-size: 0.8rem;
-  }
-}
-
 .grafana-target:last-child {
 .grafana-target:last-child {
   border-bottom: 1px solid @grafanaTargetBorder;
   border-bottom: 1px solid @grafanaTargetBorder;
 }
 }
@@ -241,7 +149,6 @@
   list-style: none;
   list-style: none;
   margin: 0;
   margin: 0;
   margin-right: 90px;
   margin-right: 90px;
-  margin-left: 30px;
   >li {
   >li {
     float: left;
     float: left;
   }
   }
@@ -249,9 +156,6 @@
 
 
 .grafana-metric-options {
 .grafana-metric-options {
   margin-top: 35px;
   margin-top: 35px;
-  .grafana-segment-list {
-    margin-left: 0;
-  }
 }
 }
 
 
 // fix for fixed positioned panel & scrolling
 // fix for fixed positioned panel & scrolling
@@ -281,6 +185,23 @@
   &a:hover {
   &a:hover {
     background: @grafanaTargetFuncBackground;
     background: @grafanaTargetFuncBackground;
   }
   }
+
+  &.template-param-name {
+    border-right: none;
+    padding-right: 3px;
+  }
+  &.annotation-segment {
+    padding: 8px 15px;
+  }
+
+}
+
+.grafana-target-segment-icon {
+  i {
+    width: 15px;
+    text-align: center;
+    display: inline-block;
+  }
 }
 }
 
 
 .grafana-target-function {
 .grafana-target-function {
@@ -306,15 +227,7 @@ input[type=text].grafana-function-param-input {
   padding: 0;
   padding: 0;
 }
 }
 
 
-.grafana-target-controls-left {
-  list-style: none;
-  float: left;
-  width: 30px;
-  margin: 0px;
-}
-
 .grafana-target-controls {
 .grafana-target-controls {
-  width: 120px;
   float: right;
   float: right;
   list-style: none;
   list-style: none;
   margin: 0;
   margin: 0;
@@ -325,10 +238,13 @@ input[type=text].grafana-function-param-input {
     white-space: nowrap;
     white-space: nowrap;
   }
   }
 
 
-  a {
-    padding: 8px 7px;
+  .icon {
     position: relative;
     position: relative;
     top: 8px;
     top: 8px;
+  }
+
+  a {
+    padding: 8px 7px;
     color: @grafanaTargetColor;
     color: @grafanaTargetColor;
     font-size: 16px;
     font-size: 16px;
 
 
@@ -350,6 +266,7 @@ input[type=text].grafana-target-text-input {
   float: left;
   float: left;
   color: @grafanaTargetColor;
   color: @grafanaTargetColor;
   border-radius: 0;
   border-radius: 0;
+  border-right: 1px solid @grafanaTargetSegmentBorder;
 }
 }
 
 
 input[type=text].grafana-target-segment-input {
 input[type=text].grafana-target-segment-input {
@@ -402,7 +319,6 @@ select.grafana-target-segment-input {
   }
   }
 }
 }
 
 
-
 .scrollable {
 .scrollable {
   max-height: 300px;
   max-height: 300px;
   overflow: auto;
   overflow: auto;
@@ -432,23 +348,22 @@ select.grafana-target-segment-input {
 ::-webkit-scrollbar-button:horizontal:increment:active { background-image: none; }
 ::-webkit-scrollbar-button:horizontal:increment:active { background-image: none; }
 ::-webkit-scrollbar-button:vertical:decrement:active { background-image: none; }
 ::-webkit-scrollbar-button:vertical:decrement:active { background-image: none; }
 ::-webkit-scrollbar-button:vertical:increment:active {background-image: none; }
 ::-webkit-scrollbar-button:vertical:increment:active {background-image: none; }
-
-::-webkit-scrollbar-track-piece { background-color: grayDark; }
+::-webkit-scrollbar-track-piece { background-color: transparent; }
 
 
 ::-webkit-scrollbar-thumb:vertical {
 ::-webkit-scrollbar-thumb:vertical {
   height: 50px;
   height: 50px;
-  background: -webkit-gradient(linear, left top, right top, color-stop(0%, #3a3a3a), color-stop(100%, #222222));
-  border: 1px solid #0d0d0d;
-  border-top: 1px solid #666666;
-  border-left: 1px solid #666666;
+  background: -webkit-gradient(linear, left top, right top, color-stop(0%, @scrollbarBackground), color-stop(100%, @scrollbarBackground2));
+  border: 1px solid @scrollbarBorder;
+  border-top: 1px solid @scrollbarBorder;
+  border-left: 1px solid @scrollbarBorder;
 }
 }
 
 
 ::-webkit-scrollbar-thumb:horizontal {
 ::-webkit-scrollbar-thumb:horizontal {
   width: 50px;
   width: 50px;
-  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3a3a3a), color-stop(100%, #222222));
-  border: 1px solid #1f1f1f;
-  border-top: 1px solid #666666;
-  border-left: 1px solid #666666;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, @scrollbarBackground), color-stop(100%, @scrollbarBackground2));
+  border: 1px solid @scrollbarBorder;
+  border-top: 1px solid @scrollbarBorder;
+  border-left: 1px solid @scrollbarBorder;
 }
 }
 
 
 
 
@@ -504,12 +419,6 @@ select.grafana-target-segment-input {
   padding: 10px;
   padding: 10px;
 }
 }
 
 
-.grafana-version-footer {
-  padding-top: 15px;
-  text-align: left;
-}
-
-
 .metrics-editor-help:hover {
 .metrics-editor-help:hover {
   .hide {
   .hide {
     display: block;
     display: block;
@@ -536,3 +445,79 @@ select.grafana-target-segment-input {
   max-width: 400px;
   max-width: 400px;
 }
 }
 
 
+.dashboard-edit-view {
+  padding: 20px;
+  background-color: @grafanaPanelBackground;
+  position: relative;
+}
+
+.dashboard-editor-body {
+  padding: 20px 10px;
+  min-height: 100px;
+}
+
+.dashboard-editor-footer {
+  overflow: hidden;
+}
+
+.dashboard-editor-header {
+  overflow: hidden;
+  .tabs {
+    float: left;
+  }
+  .nav {
+    margin: 0;
+  }
+}
+
+.dashboard-editor-title {
+  border-bottom: 1px solid @fullEditBorder;
+  padding-right: 20px;
+  float: left;
+  color: @linkColor;
+  font-size: 20px;
+  font-weight: normal;
+  line-height: 38px;
+  margin: 0;
+  .icon {
+    padding: 0 8px 0 5px;
+    color: @textColor;
+  }
+}
+
+.grafana-version-info {
+  position: absolute;
+  bottom: 2px;
+  left: 3px;
+  font-size: 80%;
+  color: darken(@gray, 25%);
+  a { color: darken(@gray, 25%); }
+}
+
+.template-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;
+}

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

@@ -5,8 +5,6 @@
 .graph-legend {
 .graph-legend {
   margin: 0 20px;
   margin: 0 20px;
   text-align: center;
   text-align: center;
-  position: relative;
-  top: 2px;
 
 
   .popover-content {
   .popover-content {
     padding: 0;
     padding: 0;
@@ -45,7 +43,7 @@
 
 
 .graph-legend-series {
 .graph-legend-series {
   padding-left: 10px;
   padding-left: 10px;
-  padding-top: 2px;
+  padding-top: 6px;
 }
 }
 
 
 .graph-legend-value {
 .graph-legend-value {

+ 22 - 76
src/css/less/overrides.less

@@ -14,7 +14,7 @@
   padding-right: 0px;
   padding-right: 0px;
 }
 }
 
 
-.container.grafana-container {
+.main-view-container {
   padding: 5px 10px;
   padding: 5px 10px;
   width: 100%;
   width: 100%;
   box-sizing: border-box;
   box-sizing: border-box;
@@ -65,74 +65,6 @@ code, pre {
   background-color: @grayLighter;
   background-color: @grayLighter;
 }
 }
 
 
-.panel {
-  display: inline-table;
-  vertical-align: top;
-}
-
-.panel-container {
-  padding: 0px 0px 0px 0px;
-  background: @grafanaPanelBackground;
-  margin: 5px;
-}
-
-.panel-content {
-  padding: 0px 10px 5px 10px;
-}
-
-.panel-title {
-  border: 0px;
-  font-weight: bold;
-}
-
-.panel-loading {
-  position:absolute;
-  top: 0px;
-  right: 4px;
-  z-index: 800;
-}
-
-.panel div.panel-extra div.panel-extra-container {
-  margin-right: -10px;
-  margin-top: 3px;
-  text-align: center;
-  ul {
-    text-align: left;
-  }
-}
-
-.panel div.panel-extra {
-  font-size: 0.9em;
-  margin-bottom: 0px;
-}
-
-.panel div.panel-extra .extra {
-  float:right !important;
-}
-
-.panel-error {
-  color: @white;
-  //padding: 5px 10px 0px 10px;
-  position: absolute;
-  left: 5px;
-  padding: 0px 17px 6px 5px;
-  top: 0;
-  i {
-    position: relative;
-    top: -2px;
-  }
-}
-.panel-error-arrow {
-  width: 0;
-  height: 0;
-  position: absolute;
-  border-left: 31px solid transparent;
-  border-right: 30px solid transparent;
-  border-bottom: 27px solid @grafanaPanelBackground;
-  left: 0;
-  bottom: 0;
-}
-
 div.editor-row {
 div.editor-row {
   vertical-align: top;
   vertical-align: top;
 }
 }
@@ -142,11 +74,13 @@ div.editor-row div.section {
   vertical-align: top;
   vertical-align: top;
   display: inline-block;
   display: inline-block;
 }
 }
+
 div.editor-option {
 div.editor-option {
   vertical-align: top;
   vertical-align: top;
   display: inline-block;
   display: inline-block;
   margin-right: 10px;
   margin-right: 10px;
 }
 }
+
 div.editor-option label {
 div.editor-option label {
   display: block;
   display: block;
 }
 }
@@ -266,9 +200,9 @@ form input.ng-invalid {
   left:-34px;
   left:-34px;
   position: absolute;
   position: absolute;
   z-index: 100;
   z-index: 100;
-  transition: .25s left;
-  transition-delay: .25s;
-  -webkit-transition-delay: .25s;
+  transition: .10s left;
+  transition-delay: .10s;
+  -webkit-transition-delay: .10s;
 }
 }
 
 
 .row-open:hover {
 .row-open:hover {
@@ -467,7 +401,6 @@ div.flot-text {
 /*************************
 /*************************
  *   Right Positions
  *   Right Positions
  *************************/
  *************************/
-
 .popover {
 .popover {
   &.rightTop .arrow {
   &.rightTop .arrow {
     top: 10%;
     top: 10%;
@@ -583,17 +516,30 @@ div.flot-text {
   }
   }
 }
 }
 
 
+// typeahead max height
+.typeahead {
+  max-height: 300px;
+  overflow-y: auto;
+}
 
 
 // Labels & Badges
 // Labels & Badges
-
 .label-tag {
 .label-tag {
   background-color: @purple;
   background-color: @purple;
   color: darken(@white, 5%);
   color: darken(@white, 5%);
+  border-radius: 2px;
+  text-shadow: none;
+  font-size: 13px;
+  padding: 4px 6px;
+  .icon-tag {
+    position: relative;
+    top: 1px;
+    padding-right: 4px;
+  }
 }
 }
 
 
 .label-tag:hover {
 .label-tag:hover {
+  opacity: 0.85;
   background-color: darken(@purple, 10%);
   background-color: darken(@purple, 10%);
-  color: @white;
 }
 }
 
 
 .annotation-editor-table {
 .annotation-editor-table {
@@ -603,7 +549,6 @@ div.flot-text {
 }
 }
 
 
 // Top menu
 // Top menu
-
 .save-dashboard-dropdown {
 .save-dashboard-dropdown {
   padding: 10px;
   padding: 10px;
   li>a {
   li>a {
@@ -632,3 +577,4 @@ code, pre {
   background-color: @grafanaPanelBackground;
   background-color: @grafanaPanelBackground;
   color: @textColor;
   color: @textColor;
 }
 }
+

+ 71 - 0
src/css/less/panel.less

@@ -0,0 +1,71 @@
+.panel {
+  display: inline-block;
+  float: left;
+  vertical-align: top;
+}
+
+.panel-container {
+  padding: 0px 0px 0px 0px;
+  background: @grafanaPanelBackground;
+  margin: 5px;
+  position: relative;
+}
+
+.panel-content {
+  padding: 0px 10px 5px 10px;
+}
+
+.panel-title {
+  border: 0px;
+  font-weight: bold;
+  position: relative;
+}
+
+.panel-loading {
+  position:absolute;
+  top: 0px;
+  right: 4px;
+  z-index: 800;
+}
+
+.panel div.panel-extra div.panel-extra-container {
+  margin-right: -10px;
+  margin-top: 3px;
+  text-align: center;
+  ul {
+    text-align: left;
+  }
+}
+
+.panel div.panel-extra {
+  font-size: 0.9em;
+  margin-bottom: 0px;
+}
+
+.panel div.panel-extra .extra {
+  float:right !important;
+}
+
+.panel-error {
+  color: @white;
+  position: absolute;
+  left: 0;
+  padding: 0px 17px 6px 5px;
+  top: 0;
+  i {
+    position: relative;
+    top: -2px;
+  }
+}
+
+.panel-error-arrow {
+  width: 0;
+  height: 0;
+  position: absolute;
+  border-left: 31px solid transparent;
+  border-right: 30px solid transparent;
+  border-bottom: 27px solid @grafanaPanelBackground;
+  left: 0;
+  bottom: 0;
+}
+

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