Browse Source

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

Conflicts:
	src/app/components/require.config.js
Torkel Ödegaard 11 years ago
parent
commit
e5219af481
100 changed files with 2530 additions and 866 deletions
  1. 28 0
      CHANGELOG.md
  2. 2 1
      src/app/app.js
  3. 27 0
      src/app/components/kbn.js
  4. 45 0
      src/app/components/panelmeta.js
  5. 3 3
      src/app/components/require.config.js
  6. 7 3
      src/app/components/settings.js
  7. 41 26
      src/app/components/timeSeries.js
  8. 21 22
      src/app/controllers/dashboardCtrl.js
  9. 10 4
      src/app/controllers/dashboardNavCtrl.js
  10. 5 1
      src/app/controllers/graphiteTarget.js
  11. 10 4
      src/app/controllers/influxTargetCtrl.js
  12. 25 15
      src/app/controllers/row.js
  13. 7 12
      src/app/controllers/sharePanelCtrl.js
  14. 0 1
      src/app/dashboards/default.json
  15. 11 0
      src/app/dashboards/scripted.js
  16. 95 0
      src/app/dashboards/scripted_gen_and_save.js
  17. 0 1
      src/app/dashboards/template_vars.json
  18. 3 4
      src/app/directives/addGraphiteFunc.js
  19. 1 1
      src/app/directives/all.js
  20. 105 0
      src/app/directives/dropdown.typeahead.js
  21. 0 132
      src/app/directives/grafanaGraph.tooltip.js
  22. 7 4
      src/app/directives/grafanaPanel.js
  23. 32 12
      src/app/directives/panelMenu.js
  24. 5 1
      src/app/directives/spectrumPicker.js
  25. 3 0
      src/app/features/all.js
  26. 39 0
      src/app/features/panellinkeditor/linkSrv.js
  27. 49 0
      src/app/features/panellinkeditor/module.html
  28. 51 0
      src/app/features/panellinkeditor/module.js
  29. 6 2
      src/app/filters/all.js
  30. 4 4
      src/app/panels/graph/axisEditor.html
  31. 22 13
      src/app/panels/graph/graph.js
  32. 182 0
      src/app/panels/graph/graph.tooltip.js
  33. 0 58
      src/app/panels/graph/legend.html
  34. 162 0
      src/app/panels/graph/legend.js
  35. 26 0
      src/app/panels/graph/legend.popover.html
  36. 15 18
      src/app/panels/graph/module.html
  37. 75 145
      src/app/panels/graph/module.js
  38. 12 3
      src/app/panels/graph/seriesOverridesCtrl.js
  39. 3 4
      src/app/panels/graph/styleEditor.html
  40. 76 0
      src/app/panels/singlestat/editor.html
  41. 26 0
      src/app/panels/singlestat/module.html
  42. 197 0
      src/app/panels/singlestat/module.js
  43. 204 0
      src/app/panels/singlestat/singleStatPanel.js
  44. 8 5
      src/app/panels/text/module.js
  45. 18 11
      src/app/panels/timepicker/module.html
  46. 2 2
      src/app/panels/timepicker/module.js
  47. 23 0
      src/app/partials/confirm_modal.html
  48. 14 18
      src/app/partials/dashboard.html
  49. 0 6
      src/app/partials/dashboard_topnav.html
  50. 1 1
      src/app/partials/dasheditor.html
  51. 51 9
      src/app/partials/graphite/editor.html
  52. 50 0
      src/app/partials/help_modal.html
  53. 21 19
      src/app/partials/influxdb/editor.html
  54. 1 0
      src/app/partials/metrics.html
  55. 26 0
      src/app/partials/opentsdb/editor.html
  56. 2 10
      src/app/partials/paneleditor.html
  57. 4 0
      src/app/partials/panelgeneral.html
  58. 1 1
      src/app/partials/search.html
  59. 12 5
      src/app/routes/dashboard-from-script.js
  60. 25 1
      src/app/services/alertSrv.js
  61. 1 1
      src/app/services/all.js
  62. 1 1
      src/app/services/annotationsSrv.js
  63. 23 2
      src/app/services/dashboard/dashboardKeyBindings.js
  64. 21 0
      src/app/services/dashboard/dashboardSrv.js
  65. 17 15
      src/app/services/dashboard/dashboardViewStateSrv.js
  66. 11 9
      src/app/services/elasticsearch/es-datasource.js
  67. 40 5
      src/app/services/graphite/gfunc.js
  68. 13 2
      src/app/services/graphite/graphiteDatasource.js
  69. 1 1
      src/app/services/influxdb/influxQueryBuilder.js
  70. 8 4
      src/app/services/influxdb/influxdbDatasource.js
  71. 5 1
      src/app/services/keyboardManager.js
  72. 19 5
      src/app/services/opentsdb/opentsdbDatasource.js
  73. 0 85
      src/app/services/panelMove.js
  74. 13 38
      src/app/services/panelSrv.js
  75. 46 0
      src/app/services/popoverSrv.js
  76. 1 0
      src/app/services/templateValuesSrv.js
  77. 12 0
      src/app/services/timeSrv.js
  78. 1 1
      src/config.sample.js
  79. 23 3
      src/css/less/grafana.less
  80. 58 17
      src/css/less/graph.less
  81. 0 4
      src/css/less/overrides.less
  82. 28 2
      src/css/less/panel.less
  83. 51 0
      src/css/less/singlestat.less
  84. 1 1
      src/css/less/submenu.less
  85. 10 0
      src/plugins/custom.panel.example/editor.html
  86. 3 0
      src/plugins/custom.panel.example/module.html
  87. 31 0
      src/plugins/custom.panel.example/module.js
  88. 1 1
      src/plugins/datasource.example.js
  89. 1 1
      src/test/specs/dashboardViewStateSrv-specs.js
  90. 1 1
      src/test/specs/gfunc-specs.js
  91. 10 5
      src/test/specs/graph-ctrl-specs.js
  92. 21 7
      src/test/specs/graph-specs.js
  93. 79 41
      src/test/specs/graph-tooltip-specs.js
  94. 1 1
      src/test/specs/graphiteDatasource-specs.js
  95. 29 0
      src/test/specs/influxQueryBuilder-specs.js
  96. 3 3
      src/test/specs/influxdb-datasource-specs.js
  97. 1 1
      src/test/specs/seriesOverridesCtrl-specs.js
  98. 12 13
      src/test/specs/sharePanelCtrl-specs.js
  99. 28 8
      src/test/specs/timeSeries-specs.js
  100. 4 5
      src/test/test-main.js

+ 28 - 0
CHANGELOG.md

@@ -3,8 +3,36 @@
 **UI Improvements*
 - [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
 
+**Graph**
 - [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
 - [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
+- [Issue #940](https://github.com/grafana/grafana/issues/940). Graph: New series style override option "Fill below to", useful to visualize max & min as a shadow for the mean
+- [Issue #1030](https://github.com/grafana/grafana/issues/1030). Graph: Legend table display/look changed, now includes column headers for min/max/avg, and full width (unless on right side)
+- [Issue #861](https://github.com/grafana/grafana/issues/861). Graph: Export graph time series data as csv file
+
+**New Panels**
+- [Issue #951](https://github.com/grafana/grafana/issues/951). SingleStat: New singlestat panel
+
+**Misc**
+- [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory
+- [Issue #952](https://github.com/grafana/grafana/issues/952). Help: Shortcut "?" to open help modal with list of all shortcuts
+- [Issue #991](https://github.com/grafana/grafana/issues/991). ScriptedDashboard: datasource services are now available in scripted dashboards, you can query datasource for metric keys, generate dashboards, and even save them in a scripted dashboard (see scripted_gen_and_save.js for example)
+- [Issue #1041](https://github.com/grafana/grafana/issues/1041). Panel: All panels can now have links to other dashboards or absolute links, these links are available in the panel menu.
+
+**Changes**
+- [Issue #1007](https://github.com/grafana/grafana/issues/1007). Graph: Series hide/show toggle changed to be default exclusive, so clicking on a series name will show only that series. (SHIFT or meta)+click will toggle hide/show.
+
+**OpenTSDB**
+- [Issue #930](https://github.com/grafana/grafana/issues/930). OpenTSDB: Adding counter max and counter reset value to open tsdb query editor, thx @rsimiciuc
+- [Issue #917](https://github.com/grafana/grafana/issues/917). OpenTSDB: Templating support for OpenTSDB series name and tags, thx @mchataigner
+
+**InfluxDB**
+- [Issue #714](https://github.com/grafana/grafana/issues/714). InfluxDB: Support for sub second resolution graphs
+
+**Fixes**
+- [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other)
+- [Issue #505](https://github.com/grafana/grafana/issues/505). Graph: fix for second y axis tick unit labels wrapping on the next line
+- [Issue #987](https://github.com/grafana/grafana/issues/987). Dashboard: Collapsed rows became invisible when hide controls was enabled
 
 =======
 # 1.8.1 (2014-09-30)

+ 2 - 1
src/app/app.js

@@ -56,7 +56,6 @@ function (angular, $, _, appLevelRequire, config) {
     register_fns.factory    = $provide.factory;
     register_fns.service    = $provide.service;
     register_fns.filter     = $filterProvider.register;
-
   });
 
   var apps_deps = [
@@ -78,6 +77,8 @@ function (angular, $, _, appLevelRequire, config) {
   });
 
   var preBootRequires = [
+    'services/all',
+    'features/all',
     'controllers/all',
     'directives/all',
     'filters/all',

+ 27 - 0
src/app/components/kbn.js

@@ -316,6 +316,10 @@ function($, _, moment) {
 
   kbn.formatFuncCreator = function(factor, extArray) {
     return function(size, decimals, scaledDecimals) {
+      if (size === null) {
+        return "";
+      }
+
       var steps = 0;
 
       while (Math.abs(size) >= factor) {
@@ -331,6 +335,10 @@ function($, _, moment) {
   };
 
   kbn.toFixed = function(value, decimals) {
+    if (value === null) {
+      return "";
+    }
+
     var factor = decimals ? Math.pow(10, decimals) : 1;
     var formatted = String(Math.round(value * factor) / factor);
 
@@ -359,6 +367,8 @@ function($, _, moment) {
   kbn.valueFormats.none = kbn.toFixed;
 
   kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
+    if (size === null) { return ""; }
+
     if (Math.abs(size) < 1000) {
       return kbn.toFixed(size, decimals) + " ms";
     }
@@ -383,6 +393,8 @@ function($, _, moment) {
   };
 
   kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
+    if (size === null) { return ""; }
+
     if (Math.abs(size) < 600) {
       return kbn.toFixed(size, decimals) + " s";
     }
@@ -407,6 +419,8 @@ function($, _, moment) {
   };
 
   kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
+    if (size === null) { return ""; }
+
     if (Math.abs(size) < 1000) {
       return kbn.toFixed(size, decimals) + " µs";
     }
@@ -419,6 +433,8 @@ function($, _, moment) {
   };
 
   kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
+    if (size === null) { return ""; }
+
     if (Math.abs(size) < 1000) {
       return kbn.toFixed(size, decimals) + " ns";
     }
@@ -443,6 +459,17 @@ function($, _, moment) {
       .replace(/ +/g,'-');
   };
 
+  kbn.exportSeriesListToCsv = function(seriesList) {
+    var text = 'Series;Time;Value\n';
+    _.each(seriesList, function(series) {
+      _.each(series.datapoints, function(dp) {
+        text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
+      });
+    });
+    var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
+    window.saveAs(blob, 'grafana_data_export.csv');
+  };
+
   kbn.stringToJsRegex = function(str) {
     if (str[0] !== '/') {
       return new RegExp(str);

+ 45 - 0
src/app/components/panelmeta.js

@@ -0,0 +1,45 @@
+define([
+],
+function () {
+  "use strict";
+
+  function PanelMeta(options) {
+    this.description = options.description;
+    this.titlePos = options.titlePos;
+    this.fullscreen = options.fullscreen;
+    this.menu = [];
+    this.editorTabs = [];
+    this.extendedMenu = [];
+
+    if (options.fullscreen) {
+      this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false)');
+    }
+
+    this.addMenuItem('edit', 'icon-cog', 'editPanel()');
+    this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()');
+    this.addMenuItem('share', 'icon-share', 'sharePanel()');
+
+    this.addEditorTab('General', 'app/partials/panelgeneral.html');
+
+    if (options.metricsEditor) {
+      this.addEditorTab('Metrics', 'app/partials/metrics.html');
+    }
+
+    this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson()');
+  }
+
+  PanelMeta.prototype.addMenuItem = function(text, icon, click) {
+    this.menu.push({text: text, icon: icon, click: click});
+  };
+
+  PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) {
+    this.extendedMenu.push({text: text, icon: icon, click: click});
+  };
+
+  PanelMeta.prototype.addEditorTab = function(title, src) {
+    this.editorTabs.push({title: title, src: src});
+  };
+
+  return PanelMeta;
+
+});

+ 3 - 3
src/app/components/require.config.js

@@ -30,7 +30,6 @@ require.config({
     bootstrap:                '../vendor/bootstrap/bootstrap',
 
     jquery:                   '../vendor/jquery/jquery-2.1.1.min',
-    'jquery-ui':              '../vendor/jquery/jquery-ui-1.10.3',
 
     'extend-jquery':          'components/extend-jquery',
 
@@ -42,6 +41,7 @@ require.config({
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
+    'jquery.flot.fillbelow':  '../vendor/jquery/jquery.flot.fillbelow',
 
     modernizr:                '../vendor/modernizr-2.6.1',
 
@@ -77,7 +77,6 @@ require.config({
 
     // simple dependency declaration
     //
-    'jquery-ui':            ['jquery'],
     'jquery.flot':          ['jquery'],
     'jquery.flot.pie':      ['jquery', 'jquery.flot'],
     'jquery.flot.events':   ['jquery', 'jquery.flot'],
@@ -86,8 +85,9 @@ require.config({
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+    'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
     'angular-cookies':      ['angular'],
-    'angular-dragdrop':     ['jquery','jquery-ui','angular'],
+    'angular-dragdrop':     ['jquery', 'angular'],
     'angular-loader':       ['angular'],
     'angular-mocks':        ['angular'],
     'angular-resource':     ['angular'],

+ 7 - 3
src/app/components/settings.js

@@ -15,12 +15,16 @@ function (_, crypto) {
     var defaults = {
       datasources                   : {},
       window_title_prefix           : 'Grafana - ',
-      panels                        : ['graph', 'text'],
+      panels                        : {
+        'graph': { path: 'panels/graph' },
+        'singlestat': { path: 'panels/singlestat' },
+        'text': { path: 'panels/text' }
+      },
       plugins                       : {},
       default_route                 : '/dashboard/file/default.json',
       playlist_timespan             : "1m",
       unsaved_changes_warning       : true,
-      search                        : { max_results: 16 },
+      search                        : { max_results: 100 },
       admin                         : {}
     };
 
@@ -76,7 +80,7 @@ function (_, crypto) {
     });
 
     if (settings.plugins.panels) {
-      settings.panels = _.union(settings.panels, settings.plugins.panels);
+      _.extend(settings.panels, settings.plugins.panels);
     }
 
     if (!settings.plugins.dependencies) {

+ 41 - 26
src/app/components/timeSeries.js

@@ -7,8 +7,12 @@ function (_, kbn) {
 
   function TimeSeries(opts) {
     this.datapoints = opts.datapoints;
-    this.info = opts.info;
-    this.label = opts.info.alias;
+    this.label = opts.alias;
+    this.id = opts.alias;
+    this.alias = opts.alias;
+    this.color = opts.color;
+    this.valueFormater = kbn.valueFormats.none;
+    this.stats = {};
   }
 
   function matchSeriesOverride(aliasOrRegex, seriesAlias) {
@@ -30,13 +34,13 @@ function (_, kbn) {
     this.lines = {};
     this.points = {};
     this.bars = {};
-    this.info.yaxis = 1;
+    this.yaxis = 1;
     this.zindex = 0;
     delete this.stack;
 
     for (var i = 0; i < overrides.length; i++) {
       var override = overrides[i];
-      if (!matchSeriesOverride(override.alias, this.info.alias)) {
+      if (!matchSeriesOverride(override.alias, this.alias)) {
         continue;
       }
       if (override.lines !== void 0) { this.lines.show = override.lines; }
@@ -48,8 +52,10 @@ function (_, kbn) {
       if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
       if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
       if (override.zindex !== void 0) { this.zindex = override.zindex; }
+      if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; }
+
       if (override.yaxis !== void 0) {
-        this.info.yaxis = override.yaxis;
+        this.yaxis = override.yaxis;
       }
     }
   };
@@ -57,12 +63,12 @@ function (_, kbn) {
   TimeSeries.prototype.getFlotPairs = function (fillStyle) {
     var result = [];
 
-    this.color = this.info.color;
-    this.yaxis = this.info.yaxis;
-
-    this.info.total = 0;
-    this.info.max = -212312321312;
-    this.info.min = 212312321312;
+    this.stats.total = 0;
+    this.stats.max = Number.MIN_VALUE;
+    this.stats.min = Number.MAX_VALUE;
+    this.stats.avg = null;
+    this.stats.current = null;
+    this.allIsNull = true;
 
     var ignoreNulls = fillStyle === 'connected';
     var nullAsZero = fillStyle === 'null as zero';
@@ -81,38 +87,47 @@ function (_, kbn) {
       }
 
       if (_.isNumber(currentValue)) {
-        this.info.total += currentValue;
+        this.stats.total += currentValue;
+        this.allIsNull = false;
       }
 
-      if (currentValue > this.info.max) {
-        this.info.max = currentValue;
+      if (currentValue > this.stats.max) {
+        this.stats.max = currentValue;
       }
 
-      if (currentValue < this.info.min) {
-        this.info.min = currentValue;
+      if (currentValue < this.stats.min) {
+        this.stats.min = currentValue;
       }
 
-      result.push([currentTime * 1000, currentValue]);
+      result.push([currentTime, currentValue]);
     }
 
-    if (result.length > 2) {
-      this.info.timeStep = result[1][0] - result[0][0];
+    if (this.datapoints.length >= 2) {
+      this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1];
     }
 
+    if (this.stats.max === Number.MIN_VALUE) { this.stats.max = null; }
+    if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
+
     if (result.length) {
-      this.info.avg = (this.info.total / result.length);
-      this.info.current = result[result.length-1][1];
+      this.stats.avg = (this.stats.total / result.length);
+      this.stats.current = result[result.length-1][1];
+      if (this.stats.current === null && result.length > 1) {
+        this.stats.current = result[result.length-2][1];
+      }
     }
 
     return result;
   };
 
   TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
-    this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals, scaledDecimals) : null;
-    this.info.current = this.info.current != null ? formater(this.info.current, decimals, scaledDecimals) : null;
-    this.info.min = this.info.min != null ? formater(this.info.min, decimals, scaledDecimals) : null;
-    this.info.max = this.info.max != null ? formater(this.info.max, decimals, scaledDecimals) : null;
-    this.info.total = this.info.total != null ? formater(this.info.total, decimals, scaledDecimals) : null;
+    this.valueFormater = formater;
+    this.decimals = decimals;
+    this.scaledDecimals = scaledDecimals;
+  };
+
+  TimeSeries.prototype.formatValue = function(value) {
+    return this.valueFormater(value, this.decimals, this.scaledDecimals);
   };
 
   return TimeSeries;

+ 21 - 22
src/app/controllers/dashboardCtrl.js

@@ -3,7 +3,6 @@ define([
   'jquery',
   'config',
   'lodash',
-  'services/all',
 ],
 function (angular, $, config, _) {
   "use strict";
@@ -18,11 +17,10 @@ function (angular, $, config, _) {
       templateValuesSrv,
       dashboardSrv,
       dashboardViewStateSrv,
-      panelMoveSrv,
       $timeout) {
 
     $scope.editor = { index: 0 };
-    $scope.panelNames = config.panels;
+    $scope.panelNames = _.map(config.panels, function(value, key) { return key; });
     var resizeEventTimeout;
 
     this.init = function(dashboardData) {
@@ -51,7 +49,6 @@ function (angular, $, config, _) {
       // init services
       timeSrv.init($scope.dashboard);
       templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
-      panelMoveSrv.init($scope.dashboard, $scope);
 
       $scope.checkFeatureToggles();
       dashboardKeybindings.shortcuts($scope);
@@ -92,21 +89,12 @@ function (angular, $, config, _) {
       };
     };
 
-    $scope.edit_path = function(type) {
-      var p = $scope.panel_path(type);
-      if(p) {
-        return p+'/editor.html';
-      } else {
-        return false;
-      }
+    $scope.panelEditorPath = function(type) {
+      return 'app/' + config.panels[type].path + '/editor.html';
     };
 
-    $scope.panel_path =function(type) {
-      if(type) {
-        return 'app/panels/'+type.replace(".","/");
-      } else {
-        return false;
-      }
+    $scope.pulldownEditorPath = function(type) {
+      return 'app/panels/'+type+'/editor.html';
     };
 
     $scope.showJsonEditor = function(evt, options) {
@@ -120,12 +108,23 @@ function (angular, $, config, _) {
       $scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable;
     };
 
-    $scope.setEditorTabs = function(panelMeta) {
-      $scope.editorTabs = ['General','Panel'];
-      if(!_.isUndefined(panelMeta.editorTabs)) {
-        $scope.editorTabs =  _.union($scope.editorTabs,_.pluck(panelMeta.editorTabs,'title'));
+    $scope.onDrop = function(panelId, row, dropTarget) {
+      var info = $scope.dashboard.getPanelInfoById(panelId);
+      if (dropTarget) {
+        var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
+        dropInfo.row.panels[dropInfo.index] = info.panel;
+        info.row.panels[info.index] = dropTarget;
+        var dragSpan = info.panel.span;
+        info.panel.span = dropTarget.span;
+        dropTarget.span = dragSpan;
       }
-      return $scope.editorTabs;
+      else {
+        info.row.panels.splice(info.index, 1);
+        info.panel.span = 12 - $scope.dashboard.rowSpan(row);
+        row.panels.push(info.panel);
+      }
+
+      $rootScope.$broadcast('render');
     };
 
   });

+ 10 - 4
src/app/controllers/dashboardNavCtrl.js

@@ -93,12 +93,18 @@ function (angular, _, moment, config, store) {
     };
 
     $scope.deleteDashboard = function(evt, options) {
-      if (!confirm('Do you want to delete dashboard ' + options.title + ' ?')) {
-        return;
-      }
-
       if (!$scope.isAdmin()) { return false; }
 
+      $scope.appEvent('confirm-modal', {
+        title: 'Delete dashboard',
+        text: 'Do you want to delete dashboard ' + options.title + '?',
+        onConfirm: function() {
+          $scope.deleteDashboardConfirmed(options);
+        }
+      });
+    };
+
+    $scope.deleteDashboardConfirmed = function(options) {
       var id = options.id;
       $scope.db.deleteDashboard(id).then(function(id) {
         $scope.appEvent('alert-success', ['Dashboard Deleted', id + ' has been deleted']);

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

@@ -201,7 +201,7 @@ function (angular, _, config, gfunc, Parser) {
 
     $scope.targetTextChanged = function() {
       parseTarget();
-      $scope.$parent.get_data();
+      $scope.get_data();
     };
 
     $scope.targetChanged = function() {
@@ -275,6 +275,10 @@ function (angular, _, config, gfunc, Parser) {
       }
     };
 
+    $scope.moveMetricQuery = function(fromIndex, toIndex) {
+      _.move($scope.panel.targets, fromIndex, toIndex);
+    };
+
     $scope.duplicate = function() {
       var clone = angular.copy($scope.target);
       $scope.panel.targets.push(clone);

+ 10 - 4
src/app/controllers/influxTargetCtrl.js

@@ -1,7 +1,8 @@
 define([
-  'angular'
+  'angular',
+  'lodash'
 ],
-function (angular) {
+function (angular, _) {
   'use strict';
 
   var module = angular.module('grafana.controllers');
@@ -83,10 +84,11 @@ function (angular) {
     };
 
     $scope.listSeries = function(query, callback) {
-      if (!seriesList || query === '') {
+      if (query !== '') {
         seriesList = [];
-        $scope.datasource.listSeries().then(function(series) {
+        $scope.datasource.listSeries(query).then(function(series) {
           seriesList = series;
+          console.log(series);
           callback(seriesList);
         });
       }
@@ -95,6 +97,10 @@ function (angular) {
       }
     };
 
+    $scope.moveMetricQuery = function(fromIndex, toIndex) {
+      _.move($scope.panel.targets, fromIndex, toIndex);
+    };
+
     $scope.duplicate = function() {
       var clone = angular.copy($scope.target);
       $scope.panel.targets.push(clone);

+ 25 - 15
src/app/controllers/row.js

@@ -47,9 +47,13 @@ function (angular, app, _) {
     };
 
     $scope.delete_row = function() {
-      if (confirm("Are you sure you want to delete this row?")) {
-        $scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
-      }
+      $scope.appEvent('confirm-modal', {
+        title: 'Delete row',
+        text: 'Are you sure you want to delete this row?',
+        onConfirm: function() {
+          $scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
+        }
+      });
     };
 
     $scope.move_row = function(direction) {
@@ -76,9 +80,13 @@ function (angular, app, _) {
     };
 
     $scope.remove_panel_from_row = function(row, panel) {
-      if (confirm('Are you sure you want to remove this ' + panel.type + ' panel?')) {
-        row.panels = _.without(row.panels,panel);
-      }
+      $scope.appEvent('confirm-modal', {
+        title: 'Remove panel',
+        text: 'Are you sure you want to remove this panel?',
+        onConfirm: function() {
+          row.panels = _.without(row.panels, panel);
+        }
+      });
     };
 
     $scope.replacePanel = function(newPanel, oldPanel) {
@@ -94,15 +102,12 @@ function (angular, app, _) {
       });
     };
 
-    $scope.duplicatePanel = function(panel, row) {
-      $scope.dashboard.duplicatePanel(panel, row || $scope.row);
-    };
-
     $scope.reset_panel = function(type) {
       var defaultSpan = 12;
       var _as = 12 - $scope.dashboard.rowSpan($scope.row);
 
       $scope.panel = {
+        title: 'no title [click here]',
         error   : false,
         span    : _as < defaultSpan && _as > 0 ? _as : defaultSpan,
         editable: true,
@@ -144,13 +149,18 @@ function (angular, app, _) {
 
   module.directive('panelDropZone', function() {
     return function(scope, element) {
-      scope.$watch('dashboard.$$panelDragging', function(newVal) {
-        if (newVal && scope.dashboard.rowSpan(scope.row) < 10) {
+      scope.$on("ANGULAR_DRAG_START", function() {
+        var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row);
+
+        if (dropZoneSpan > 0) {
+          element.find('.panel-container').css('height', scope.row.height);
+          element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%';
           element.show();
         }
-        else {
-          element.hide();
-        }
+      });
+
+      scope.$on("ANGULAR_DRAG_END", function() {
+        element.hide();
       });
     };
   });

+ 7 - 12
src/app/controllers/sharePanelCtrl.js

@@ -27,19 +27,12 @@ function (angular, _) {
       }
 
       var panelId = $scope.panel.id;
-      var range = timeSrv.timeRange(false);
       var params = angular.copy($location.search());
 
-      if (_.isString(range.to) && range.to.indexOf('now')) {
-        range = timeSrv.timeRange();
-      }
-
+      var range = timeSrv.timeRangeForUrl();
       params.from = range.from;
       params.to = range.to;
 
-      if (_.isDate(params.from)) { params.from = params.from.getTime(); }
-      if (_.isDate(params.to)) { params.to = params.to.getTime(); }
-
       if ($scope.includeTemplateVars) {
         _.each(templateSrv.variables, function(variable) {
           params['var-' + variable.name] = variable.current.text;
@@ -66,11 +59,13 @@ function (angular, _) {
 
       var paramsArray = [];
       _.each(params, function(value, key) {
-        var str = key;
-        if (value !== true) {
-          str += '=' + encodeURIComponent(value);
+        if (value === null) { return; }
+        if (value === true) {
+          paramsArray.push(key);
+        } else {
+          key += '=' + encodeURIComponent(value);
+          paramsArray.push(key);
         }
-        paramsArray.push(str);
       });
 
       $scope.shareUrl = baseUrl + "?" + paramsArray.join('&') ;

+ 0 - 1
src/app/dashboards/default.json

@@ -101,7 +101,6 @@
           "legend_counts": true,
           "timezone": "browser",
           "percentage": false,
-          "zerofill": true,
           "nullPointMode": "connected",
           "steppedLine": false,
           "tooltip": {

+ 11 - 0
src/app/dashboards/scripted.js

@@ -68,6 +68,17 @@ for (var i = 0; i < rows; i++) {
             'target': "randomWalk('random walk2')"
           }
         ],
+        seriesOverrides: [
+          {
+            alias: '/random/',
+            yaxis: 2,
+            fill: 0,
+            linewidth: 5
+          }
+        ],
+        tooltip: {
+          shared: true
+        }
       }
     ]
   });

+ 95 - 0
src/app/dashboards/scripted_gen_and_save.js

@@ -0,0 +1,95 @@
+/* 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, services, _;
+
+// default datasource
+var datasource = services.datasourceSrv.default;
+// get datasource used for saving dashboards
+var dashboardDB = services.datasourceSrv.getGrafanaDB();
+
+var targets = [];
+
+function getTargets(path) {
+  return datasource.metricFindQuery(path + '.*').then(function(result) {
+    if (!result) {
+      return null;
+    }
+
+    if (targets.length === 10) {
+      return null;
+    }
+
+    var promises = _.map(result, function(metric) {
+      if (metric.expandable) {
+        return getTargets(path + "." + metric.text);
+      }
+      else {
+        targets.push(path + '.' + metric.text);
+      }
+      return null;
+    });
+
+    return services.$q.all(promises);
+  });
+}
+
+function createDashboard(target, index) {
+  // Intialize a skeleton with nothing but a rows array and service object
+  var dashboard = { rows : [] };
+  dashboard.title = 'Scripted dash ' + index;
+  dashboard.time = {
+    from: "now-6h",
+    to: "now"
+  };
+
+  dashboard.rows.push({
+    title: 'Chart',
+    height: '300px',
+    panels: [
+    {
+      title: 'Events',
+      type: 'graph',
+      span: 12,
+      targets: [ {target: target} ]
+    }
+  ]
+  });
+
+  return dashboard;
+}
+
+function saveDashboard(dashboard) {
+  var model = services.dashboardSrv.create(dashboard);
+  dashboardDB.saveDashboard(model);
+}
+
+return function(callback)  {
+
+  getTargets('apps').then(function() {
+    console.log('targets: ', targets);
+    _.each(targets, function(target, index) {
+      var dashboard = createDashboard(target, index);
+      saveDashboard(dashboard);
+
+      if (index === targets.length - 1) {
+        callback(dashboard);
+      }
+    });
+  });
+
+};
+

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

@@ -65,7 +65,6 @@
             "avg": false
           },
           "percentage": false,
-          "zerofill": true,
           "nullPointMode": "connected",
           "steppedLine": false,
           "tooltip": {

+ 3 - 4
src/app/directives/addGraphiteFunc.js

@@ -68,13 +68,12 @@ function (angular, app, _, $, gfunc) {
           });
 
           $input.blur(function() {
-            $input.hide();
-            $input.val('');
-            $button.show();
-            $button.focus();
             // clicking the function dropdown menu wont
             // work if you remove class at once
             setTimeout(function() {
+              $input.val('');
+              $input.hide();
+              $button.show();
               elem.removeClass('open');
             }, 200);
           });

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

@@ -10,7 +10,6 @@ define([
   './confirmClick',
   './configModal',
   './spectrumPicker',
-  './grafanaGraph',
   './bootstrap-tagsinput',
   './bodyClass',
   './addGraphiteFunc',
@@ -18,5 +17,6 @@ define([
   './templateParamSelector',
   './graphiteSegment',
   './grafanaVersionCheck',
+  './dropdown.typeahead',
   './influxdbFuncEditor'
 ], function () {});

+ 105 - 0
src/app/directives/dropdown.typeahead.js

@@ -0,0 +1,105 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('dropdownTypeahead', function($compile) {
+
+      var inputTemplate = '<input type="text"'+
+                            ' class="grafana-target-segment-input input-medium grafana-target-segment-input"' +
+                            ' spellcheck="false" style="display:none"></input>';
+
+      var buttonTemplate = '<a  class="grafana-target-segment grafana-target-function dropdown-toggle"' +
+                              ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
+                              ' data-placement="top"><i class="icon-plus"></i></a>';
+
+      return {
+        scope: {
+          "menuItems": "=dropdownTypeahead",
+          "dropdownTypeaheadOnSelect": "&dropdownTypeaheadOnSelect"
+        },
+        link: function($scope, elem) {
+          var $input = $(inputTemplate);
+          var $button = $(buttonTemplate);
+          $input.appendTo(elem);
+          $button.appendTo(elem);
+
+          var typeaheadValues = _.reduce($scope.menuItems, function(memo, value) {
+            _.each(value.submenu, function(item) {
+              memo.push(value.text + ' ' + item.text);
+            });
+            return memo;
+          }, []);
+
+          $scope.menuItemSelected = function(optionIndex, valueIndex) {
+            var option = $scope.menuItems[optionIndex];
+            var result = {
+              $item: option.submenu[valueIndex],
+              $optionIndex: optionIndex,
+              $valueIndex: valueIndex
+            };
+
+            $scope.dropdownTypeaheadOnSelect(result);
+          };
+
+          $input.attr('data-provide', 'typeahead');
+          $input.typeahead({
+            source: typeaheadValues,
+            minLength: 1,
+            items: 10,
+            updater: function (value) {
+              var result = {};
+              _.each($scope.menuItems, function(menuItem, optionIndex) {
+                _.each(menuItem.submenu, function(submenuItem, valueIndex) {
+                  if (value === (menuItem.text + ' ' + submenuItem.text)) {
+                    result.$item  = submenuItem;
+                    result.$optionIndex = optionIndex;
+                    result.$valueIndex = valueIndex;
+                  }
+                });
+              });
+
+              if (result.$item) {
+                $scope.$apply(function() {
+                  $scope.dropdownTypeaheadOnSelect(result);
+                });
+              }
+
+              $input.trigger('blur');
+              return '';
+            }
+          });
+
+          $button.click(function() {
+            $button.hide();
+            $input.show();
+            $input.focus();
+          });
+
+          $input.keyup(function() {
+            elem.toggleClass('open', $input.val() === '');
+          });
+
+          $input.blur(function() {
+            $input.hide();
+            $input.val('');
+            $button.show();
+            $button.focus();
+            // clicking the function dropdown menu wont
+            // work if you remove class at once
+            setTimeout(function() {
+              elem.removeClass('open');
+            }, 200);
+          });
+
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});

+ 0 - 132
src/app/directives/grafanaGraph.tooltip.js

@@ -1,132 +0,0 @@
-define([
-  'jquery',
-  'kbn',
-],
-function ($, kbn) {
-  'use strict';
-
-  function registerTooltipFeatures(elem, dashboard, scope) {
-
-    var $tooltip = $('<div id="tooltip">');
-
-    elem.mouseleave(function () {
-      if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
-        var plot = elem.data().plot;
-        if (plot) {
-          $tooltip.detach();
-          plot.unhighlight();
-          scope.appEvent('clearCrosshair');
-        }
-      }
-    });
-
-    function findHoverIndex(posX, series) {
-      for (var j = 0; j < series.data.length; j++) {
-        if (series.data[j][0] > posX) {
-          return Math.max(j - 1,  0);
-        }
-      }
-      return j - 1;
-    }
-
-    function showTooltip(title, innerHtml, pos) {
-      var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
-      body += innerHtml + '</div>';
-      $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
-    }
-
-    elem.bind("plothover", function (event, pos, item) {
-      var plot = elem.data().plot;
-      var data = plot.getData();
-      var group, value, timestamp, seriesInfo, format, i, series, hoverIndex, seriesHtml;
-
-      if(dashboard.sharedCrosshair){
-        scope.appEvent('setCrosshair',  { pos: pos, scope: scope });
-      }
-
-      if (scope.panel.tooltip.shared) {
-        plot.unhighlight();
-
-        //check if all series has same length if so, only one x index will
-        //be checked and only for exact timestamp values
-        var pointCount = data[0].data.length;
-        for (i = 1; i < data.length; i++) {
-          if (data[i].data.length !== pointCount) {
-            showTooltip('Shared tooltip error', '<ul>' +
-              '<li>Series point counts are not the same</li>' +
-              '<li>Set null point mode to null or null as zero</li>' +
-              '<li>For influxdb users set fill(0) in your query</li></ul>', pos);
-            return;
-          }
-        }
-
-        seriesHtml = '';
-        series = data[0];
-        hoverIndex = findHoverIndex(pos.x, series);
-
-        //now we know the current X (j) position for X and Y values
-        timestamp = dashboard.formatDate(series.data[hoverIndex][0]);
-        var last_value = 0; //needed for stacked values
-
-        for (i = data.length-1; i >= 0; --i) {
-          //stacked values should be added in reverse order
-          series = data[i];
-          seriesInfo = series.info;
-          format = scope.panel.y_formats[seriesInfo.yaxis - 1];
-
-          if (scope.panel.stack) {
-            if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
-              value = series.data[hoverIndex][1];
-            } else {
-              last_value += series.data[hoverIndex][1];
-              value = last_value;
-            }
-          } else {
-            value = series.data[hoverIndex][1];
-          }
-
-          value = kbn.valueFormats[format](value, series.yaxis.tickDecimals);
-
-          if (seriesInfo.alias) {
-            group = '<i class="icon-minus" style="color:' + series.color +';"></i> ' + seriesInfo.alias;
-          } else {
-            group = kbn.query_color_dot(series.color, 15) + ' ';
-          }
-
-          //pre-pending new values
-          seriesHtml = group + ': <span class="graph-tooltip-value">' + value + '</span><br>' + seriesHtml;
-
-          plot.highlight(i, hoverIndex);
-        }
-
-        showTooltip(timestamp, seriesHtml, pos);
-        return;
-      }
-      if (item) {
-        seriesInfo = item.series.info;
-        format = scope.panel.y_formats[seriesInfo.yaxis - 1];
-        group = '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + seriesInfo.alias;
-
-        if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
-          value = item.datapoint[1] - item.datapoint[2];
-        }
-        else {
-          value = item.datapoint[1];
-        }
-
-        value = kbn.valueFormats[format](value, item.series.yaxis.tickDecimals);
-        timestamp = dashboard.formatDate(item.datapoint[0]);
-        group += ': <span class="graph-tooltip-value">' + value + '</span>';
-
-        showTooltip(timestamp, group, pos);
-      } else {
-        $tooltip.detach();
-      }
-    });
-
-  }
-
-  return {
-    register: registerTooltipFeatures
-  };
-});

+ 7 - 4
src/app/directives/grafanaPanel.js

@@ -1,9 +1,10 @@
 define([
   'angular',
   'jquery',
+  'config',
   './panelMenu',
 ],
-function (angular, $) {
+function (angular, $, config) {
   'use strict';
 
   angular
@@ -26,7 +27,7 @@ function (angular, $) {
             '<i class="icon-spinner icon-spin icon-large"></i>' +
           '</span>' +
 
-          '<div class="panel-title-container" panel-menu></div>' +
+          '<div class="panel-title-container drag-handle" panel-menu></div>' +
         '</div>'+
       '</div>';
 
@@ -68,10 +69,12 @@ function (angular, $) {
 
           elem.addClass('ng-cloak');
 
+          var panelPath = config.panels[panelType].path;
+
           $scope.require([
             'jquery',
-            'text!panels/'+panelType+'/module.html',
-            'panels/' + panelType + "/module",
+            'text!'+panelPath+'/module.html',
+            panelPath + "/module",
           ], function ($, moduleTemplate) {
             var $module = $(moduleTemplate);
             $module.prepend(panelHeader);

+ 32 - 12
src/app/directives/panelMenu.js

@@ -8,16 +8,12 @@ function (angular, $, _) {
 
   angular
     .module('grafana.directives')
-    .directive('panelMenu', function($compile) {
-      var linkTemplate = '<a class="panel-title">{{panel.title | interpolateTemplateVars}}</a>';
-      var moveAttributes = ' data-drag=true data-jqyoui-options="kbnJqUiDraggableOptions"'+
-              ' jqyoui-draggable="{'+
-                'animate:false,'+
-                'mutate:false,'+
-                'index:{{$index}},'+
-                'onStart:\'panelMoveStart\','+
-                'onStop:\'panelMoveStop\''+
-                '}"  ng-model="panel" ';
+    .directive('panelMenu', function($compile, linkSrv) {
+      var linkTemplate =
+          '<span class="panel-title drag-handle pointer">' +
+            '<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars}}</span>' +
+            '<span class="panel-links-icon"></span>' +
+          '</span>';
 
       function createMenuTemplate($scope) {
         var template = '<div class="panel-menu small">';
@@ -26,11 +22,11 @@ function (angular, $, _) {
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="icon-minus"></i></a>';
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="icon-plus"></i></a>';
         template += '<a class="panel-menu-icon pull-right" ng-click="remove_panel_from_row(row, panel)"><i class="icon-remove"></i></a>';
-        template += '<a class="panel-menu-icon pull-right" ' + moveAttributes + '><i class="icon-move"></i></a>';
         template += '<div class="clearfix"></div>';
         template += '</div>';
 
         template += '<div class="panel-menu-row">';
+        template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="icon-th-list"></i></a>';
 
         _.each($scope.panelMeta.menu, function(item) {
           template += '<a class="panel-menu-link" ';
@@ -46,18 +42,36 @@ function (angular, $, _) {
         return template;
       }
 
+      function getExtendedMenu($scope) {
+        var menu = angular.copy($scope.panelMeta.extendedMenu);
+
+        if ($scope.panel.links) {
+          _.each($scope.panel.links, function(link) {
+            var info = linkSrv.getPanelLinkAnchorInfo(link);
+            menu.push({text: info.title, href: info.href, target: info.target });
+          });
+        }
+
+        return menu;
+      }
+
       return {
         restrict: 'A',
         link: function($scope, elem) {
           var $link = $(linkTemplate);
           var $panelContainer = elem.parents(".panel-container");
-          var menuWidth = $scope.panelMeta.menu.length === 5 ? 246 : 201;
+          var menuWidth = $scope.panelMeta.menu.length === 4 ? 236 : 191;
           var menuScope = null;
           var timeout = null;
           var $menu = null;
 
           elem.append($link);
 
+          $scope.$watchCollection('panel.links', function(newValue) {
+            var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
+            $link.toggleClass('has-panel-links', showIcon);
+          });
+
           function dismiss(time) {
             clearTimeout(timeout);
             timeout = null;
@@ -109,6 +123,7 @@ function (angular, $, _) {
             });
 
             menuScope = $scope.$new();
+            menuScope.extendedMenu = getExtendedMenu($scope);
 
             $('.panel-menu').remove();
             elem.append($menu);
@@ -122,6 +137,11 @@ function (angular, $, _) {
             dismiss(2500);
           };
 
+          if ($scope.panelMeta.titlePos && $scope.panel.title) {
+            elem.css('text-align', 'left');
+            $link.css('padding-left', '10px');
+          }
+
           elem.click(showMenu);
           $compile(elem.contents())($scope);
         }

+ 5 - 1
src/app/directives/spectrumPicker.js

@@ -32,7 +32,11 @@ function (angular) {
           };
 
           input.spectrum(options);
+
+          scope.$on('$destroy', function() {
+            input.spectrum('destroy');
+          });
         }
       };
     });
-});
+});

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

@@ -0,0 +1,3 @@
+define([
+  './panellinkeditor/module',
+], function () {});

+ 39 - 0
src/app/features/panellinkeditor/linkSrv.js

@@ -0,0 +1,39 @@
+define([
+  'angular',
+  'kbn',
+],
+function (angular, kbn) {
+  'use strict';
+
+  angular
+    .module('grafana.services')
+    .service('linkSrv', function(templateSrv, timeSrv) {
+
+      this.getPanelLinkAnchorInfo = function(link) {
+        var info = {};
+        if (link.type === 'absolute') {
+          info.target = '_blank';
+          info.href = templateSrv.replace(link.url || '');
+          info.title = templateSrv.replace(link.title || '');
+          info.href += '?';
+
+        }
+        else {
+          info.title = templateSrv.replace(link.title || '');
+          var slug = kbn.slugifyForUrl(link.dashboard || '');
+          info.href = '#dashboard/db/' + slug + '?';
+        }
+
+        var range = timeSrv.timeRangeForUrl();
+        info.href += 'from=' + range.from;
+        info.href += '&to=' + range.to;
+
+        if (link.params) {
+          info.href += "&" + link.params;
+        }
+
+        return info;
+      };
+
+    });
+});

+ 49 - 0
src/app/features/panellinkeditor/module.html

@@ -0,0 +1,49 @@
+<div class="editor-row">
+  <div class="section">
+		<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu</tip></h5>
+
+		<div class="grafana-target" ng-repeat="link in panel.links"j>
+			<div class="grafana-target-inner">
+				<ul class="grafana-segment-list">
+					<li class="grafana-target-segment">
+						<i class="icon-remove pointer" ng-click="deleteLink(link)"></i>
+					</li>
+
+					<li class="grafana-target-segment">title</li>
+					<li>
+						<input type="text" ng-model="link.title" class="input-medium grafana-target-segment-input">
+					</li>
+
+					<li class="grafana-target-segment">type</li>
+					<li>
+						<select class="input-medium grafana-target-segment-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
+					</li>
+
+					<li class="grafana-target-segment" ng-show="link.type === 'dashboard'">dashboard</li>
+					<li ng-show="link.type === 'dashboard'">
+						<input type="text"
+						       ng-model="link.dashboard"
+									 bs-typeahead="searchDashboards"
+									 class="input-large grafana-target-segment-input">
+					</li>
+
+					<li class="grafana-target-segment" ng-show="link.type === 'absolute'">url</li>
+					<li ng-show="link.type === 'absolute'">
+						<input type="text" ng-model="link.url" class="input-large grafana-target-segment-input">
+					</li>
+
+					<li class="grafana-target-segment">params</li>
+					<li>
+						<input type="text" ng-model="link.params" class="input-medium grafana-target-segment-input">
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="editor-row">
+	<br>
+	<button class="btn btn-success" ng-click="addLink()">Add link</button>
+</div>

+ 51 - 0
src/app/features/panellinkeditor/module.js

@@ -0,0 +1,51 @@
+define([
+  'angular',
+  'lodash',
+  './linkSrv',
+],
+function (angular, _) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('panelLinkEditor', function() {
+      return {
+        scope: {
+          panel: "="
+        },
+        restrict: 'E',
+        controller: 'PanelLinkEditorCtrl',
+        templateUrl: 'app/features/panellinkeditor/module.html',
+        link: function() {
+        }
+      };
+    }).controller('PanelLinkEditorCtrl', function($scope, datasourceSrv) {
+
+      $scope.panel.links = $scope.panel.links || [];
+
+      $scope.addLink = function() {
+        $scope.panel.links.push({
+          type: 'dashboard',
+          name: 'Drilldown dashboard'
+        });
+      };
+
+      $scope.searchDashboards = function(query, callback) {
+        var ds = datasourceSrv.getGrafanaDB();
+        if (ds === null) { return; }
+
+        ds.searchDashboards(query).then(function(result) {
+          var dashboards = _.map(result.dashboards, function(dash) {
+            return dash.title;
+          });
+
+          callback(dashboards);
+        });
+      };
+
+      $scope.deleteLink = function(link) {
+        $scope.panel.links = _.without($scope.panel.links, link);
+      };
+
+    });
+});

+ 6 - 2
src/app/filters/all.js

@@ -56,9 +56,13 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
   });
 
   module.filter('interpolateTemplateVars', function(templateSrv) {
-    return function(text) {
+    function interpolateTemplateVars(text) {
       return templateSrv.replaceWithText(text);
-    };
+    }
+
+    interpolateTemplateVars.$stateful = true;
+
+    return interpolateTemplateVars;
   });
 
 });

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

@@ -40,11 +40,11 @@
 <div class="editor-row">
   <div class="section">
     <h5>Legend styles</h5>
-		<editor-opt-bool text="Show legend" model="panel.legend.show" change="render()"></editor-opt-bool>
-		<editor-opt-bool text="Include values" model="panel.legend.values" change="render()"></editor-opt-bool>
-		<editor-opt-bool text="Align as table" model="panel.legend.alignAsTable" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Show" model="panel.legend.show" change="get_data();"></editor-opt-bool>
+		<editor-opt-bool text="Values" model="panel.legend.values" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Table" model="panel.legend.alignAsTable" change="render()"></editor-opt-bool>
 		<editor-opt-bool text="Right side" model="panel.legend.rightSide" change="render()"></editor-opt-bool>
-  </div>
+	</div>
 
   <div class="section" ng-if="panel.legend.values">
     <h5>Legend values</h5>

+ 22 - 13
src/app/directives/grafanaGraph.js → src/app/panels/graph/graph.js

@@ -4,9 +4,17 @@ define([
   'kbn',
   'moment',
   'lodash',
-  './grafanaGraph.tooltip'
+  './graph.tooltip',
+  'jquery.flot',
+  'jquery.flot.events',
+  'jquery.flot.selection',
+  'jquery.flot.time',
+  'jquery.flot.stack',
+  'jquery.flot.stackpercent',
+  'jquery.flot.fillbelow',
+  'jquery.flot.crosshair'
 ],
-function (angular, $, kbn, moment, _, graphTooltip) {
+function (angular, $, kbn, moment, _, GraphTooltip) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -46,10 +54,6 @@ function (angular, $, kbn, moment, _, graphTooltip) {
           scope.get_data();
         });
 
-        scope.$on('toggleLegend', function() {
-          render_panel();
-        });
-
         // Receive render events
         scope.$on('render',function(event, renderData) {
           data = renderData || data;
@@ -68,7 +72,7 @@ function (angular, $, kbn, moment, _, graphTooltip) {
               height = parseInt(height.replace('px', ''), 10);
             }
 
-            height = height - 32; // subtract panel title bar
+            height -= scope.panel.title ? 24 : 9; // subtract panel title bar
 
             if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
               height = height - 21; // subtract one line legend
@@ -110,9 +114,9 @@ function (angular, $, kbn, moment, _, graphTooltip) {
             var series = data[i];
             var axis = yaxis[series.yaxis - 1];
             var formater = kbn.valueFormats[scope.panel.y_formats[series.yaxis - 1]];
-            series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals);
+            series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals + 2);
+            if(!scope.$$phase) { scope.$digest(); }
           }
-
         }
 
         // Function for rendering panel
@@ -177,15 +181,16 @@ function (angular, $, kbn, moment, _, graphTooltip) {
             var series = data[i];
             series.applySeriesOverrides(panel.seriesOverrides);
             series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
+
             // if hidden remove points and disable stack
-            if (scope.hiddenSeries[series.info.alias]) {
+            if (scope.hiddenSeries[series.alias]) {
               series.data = [];
               series.stack = false;
             }
           }
 
-          if (data.length && data[0].info.timeStep) {
-            options.series.bars.barWidth = data[0].info.timeStep / 1.5;
+          if (data.length && data[0].stats.timeStep) {
+            options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
           }
 
           addTimeAxis(options);
@@ -206,6 +211,8 @@ function (angular, $, kbn, moment, _, graphTooltip) {
           }
 
           if (shouldDelayDraw(panel)) {
+            // temp fix for legends on the side, need to render twice to get dimensions right
+            callPlot();
             setTimeout(callPlot, 50);
             legendSideLastValue = panel.legend.rightSide;
           }
@@ -416,7 +423,9 @@ function (angular, $, kbn, moment, _, graphTooltip) {
           elem.html('<img src="' + url + '"></img>');
         }
 
-        graphTooltip.register(elem, dashboard, scope, $rootScope);
+        new GraphTooltip(elem, dashboard, scope, function() {
+          return data;
+        });
 
         elem.bind("plotselected", function (event, ranges) {
           scope.$apply(function() {

+ 182 - 0
src/app/panels/graph/graph.tooltip.js

@@ -0,0 +1,182 @@
+define([
+  'jquery',
+],
+function ($) {
+  'use strict';
+
+  function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
+    var self = this;
+
+    var $tooltip = $('<div id="tooltip">');
+
+    this.findHoverIndexFromDataPoints = function(posX, series,last) {
+      var ps = series.datapoints.pointsize;
+      var initial = last*ps;
+      var len = series.datapoints.points.length;
+      for (var j = initial; j < len; j += ps) {
+        if (series.datapoints.points[j] > posX) {
+          return Math.max(j - ps,  0)/ps;
+        }
+      }
+      return j/ps - 1;
+    };
+
+    this.findHoverIndexFromData = function(posX, series) {
+      var len = series.data.length;
+      for (var j = 0; j < len; j++) {
+        if (series.data[j][0] > posX) {
+          return Math.max(j - 1,  0);
+        }
+      }
+      return j - 1;
+    };
+
+    this.showTooltip = function(title, innerHtml, pos) {
+      var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
+      body += innerHtml + '</div>';
+      $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
+    };
+
+    this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
+      var value, i, series, hoverIndex;
+      var results = [];
+
+      var pointCount = seriesList[0].data.length;
+      for (i = 1; i < seriesList.length; i++) {
+        if (seriesList[i].data.length !== pointCount) {
+          results.pointCountMismatch = true;
+          return results;
+        }
+      }
+
+      series = seriesList[0];
+      hoverIndex = this.findHoverIndexFromData(pos.x, series);
+      var lasthoverIndex = 0;
+      if(!scope.panel.steppedLine) {
+        lasthoverIndex = hoverIndex;
+      }
+
+      //now we know the current X (j) position for X and Y values
+      results.time = series.data[hoverIndex][0];
+      var last_value = 0; //needed for stacked values
+
+      for (i = 0; i < seriesList.length; i++) {
+        series = seriesList[i];
+
+        if (scope.panel.stack) {
+          if (scope.panel.tooltip.value_type === 'individual') {
+            value = series.data[hoverIndex][1];
+          } else {
+            last_value += series.data[hoverIndex][1];
+            value = last_value;
+          }
+        } else {
+          value = series.data[hoverIndex][1];
+        }
+
+        // Highlighting multiple Points depending on the plot type
+        if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) {
+          // stacked and steppedLine plots can have series with different length.
+          // Stacked series can increase its length  on each new stacked serie if null points found,
+          // to speed the index search we begin always on the las found hoverIndex.
+          var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex);
+          // update lasthoverIndex depends also on the plot type.
+          if(!scope.panel.steppedLine) {
+            // on stacked graphs new will be always greater than last
+            lasthoverIndex = newhoverIndex;
+          } else {
+            // if steppeLine, not always series increases its length, so we should begin
+            // to search correct index from the original hoverIndex on each serie.
+            lasthoverIndex = hoverIndex;
+          }
+
+          results.push({ value: value, hoverIndex: newhoverIndex });
+        } else {
+          results.push({ value: value, hoverIndex: hoverIndex });
+        }
+      }
+
+      return results;
+    };
+
+    elem.mouseleave(function () {
+      if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
+        var plot = elem.data().plot;
+        if (plot) {
+          $tooltip.detach();
+          plot.unhighlight();
+          scope.appEvent('clearCrosshair');
+        }
+      }
+    });
+
+    elem.bind("plothover", function (event, pos, item) {
+      var plot = elem.data().plot;
+      var plotData = plot.getData();
+      var seriesList = getSeriesFn();
+      var group, value, timestamp, hoverInfo, i, series, seriesHtml;
+
+      if(dashboard.sharedCrosshair){
+        scope.appEvent('setCrosshair',  { pos: pos, scope: scope });
+      }
+
+      if (seriesList.length === 0) {
+        return;
+      }
+
+      if (scope.panel.tooltip.shared) {
+        plot.unhighlight();
+
+        var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
+        if (seriesHoverInfo.pointCountMismatch) {
+          self.showTooltip('Shared tooltip error', '<ul>' +
+            '<li>Series point counts are not the same</li>' +
+            '<li>Set null point mode to null or null as zero</li>' +
+            '<li>For influxdb users set fill(0) in your query</li></ul>', pos);
+          return;
+        }
+
+        seriesHtml = '';
+        timestamp = dashboard.formatDate(seriesHoverInfo.time);
+
+        for (i = 0; i < seriesHoverInfo.length; i++) {
+          series = seriesList[i];
+          hoverInfo = seriesHoverInfo[i];
+          value = series.formatValue(hoverInfo.value);
+
+          seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
+          seriesHtml += '<i class="icon-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
+          seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
+          plot.highlight(i, hoverInfo.hoverIndex);
+        }
+
+        self.showTooltip(timestamp, seriesHtml, pos);
+      }
+      // single series tooltip
+      else if (item) {
+        series = seriesList[item.seriesIndex];
+        group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
+        group += '<i class="icon-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
+
+        if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
+          value = item.datapoint[1] - item.datapoint[2];
+        }
+        else {
+          value = item.datapoint[1];
+        }
+
+        value = series.formatValue(value);
+        timestamp = dashboard.formatDate(item.datapoint[0]);
+        group += '<div class="graph-tooltip-value">' + value + '</div>';
+
+        self.showTooltip(timestamp, group, pos);
+      }
+      // no hit
+      else {
+        $tooltip.detach();
+      }
+    });
+  }
+
+  return GraphTooltip;
+});

+ 0 - 58
src/app/panels/graph/legend.html

@@ -1,58 +0,0 @@
-<section class="graph-legend" ng-class="{'graph-legend-table': panel.legend.alignAsTable}">
-
-  <div class="graph-legend-series"
-       ng-repeat='series in legend'
-       ng-class="{'pull-right': series.yaxis === 2, 'graph-legend-series-hidden': hiddenSeries[series.alias]}"
-       >
-    <div class="graph-legend-icon">
-      <i class='icon-minus pointer' ng-style="{color: series.color}" bs-popover="'colorPopup.html'" data-placement="bottom">
-      </i>
-    </div>
-    <div class="graph-legend-alias small">
-      <a ng-click="toggleSeries(series, $event)" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
-        {{series.alias}}
-      </a>
-    </div>
-    <div class="graph-legend-value current small" ng-show="panel.legend.values && panel.legend.current" ng-bind="series.current">
-    </div>
-    <div class="graph-legend-value min small" ng-show="panel.legend.values && panel.legend.min" ng-bind="series.min">
-    </div>
-    <div class="graph-legend-value max small" ng-show="panel.legend.values && panel.legend.max" ng-bind="series.max">
-    </div>
-    <div class="graph-legend-value total small" ng-show="panel.legend.values && panel.legend.total" ng-bind="series.total">
-    </div>
-    <div class="graph-legend-value avg small" ng-show="panel.legend.values && panel.legend.avg" ng-bind="series.avg">
-    </div>
-  </div>
-
-</section>
-
-
-<script type="text/ng-template" id="colorPopup.html">
-  <div class="graph-legend-popover">
-    <a class="close" ng-click="dismiss();" href="">×</a>
-
-    <div class="editor-row small" style="padding-bottom: 0;">
-      <label>Axis:</label>
-      <button ng-click="toggleYAxis(series);dismiss();"
-              class="btn btn-mini"
-              ng-class="{'btn-success': series.yaxis === 1 }">
-        Left
-      </button>
-      <button ng-click="toggleYAxis(series);dismiss();"
-              class="btn btn-mini"
-              ng-class="{'btn-success': series.yaxis === 2 }">
-        Right
-      </button>
-    </div>
-
-    <div class="editor-row">
-      <i ng-repeat="color in colors"
-        class="pointer"
-        ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
-        ng-style="{color:color}"
-        ng-click="changeSeriesColor(series, color);dismiss();">
-      </i>
-    </div>
-  </div>
-</script>

+ 162 - 0
src/app/panels/graph/legend.js

@@ -0,0 +1,162 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'kbn',
+  'jquery',
+  'jquery.flot',
+  'jquery.flot.time',
+],
+function (angular, app, _, kbn, $) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.graph');
+
+  module.directive('graphLegend', function(popoverSrv) {
+
+    return {
+      link: function(scope, elem) {
+        var $container = $('<section class="graph-legend"></section>');
+        var firstRender = true;
+        var panel = scope.panel;
+        var data;
+        var seriesList;
+        var i;
+
+        scope.$on('render', function() {
+          data = scope.seriesList;
+          if (data) {
+            render();
+          }
+        });
+
+        function getSeriesIndexForElement(el) {
+          return el.parents('[data-series-index]').data('series-index');
+        }
+
+        function openColorSelector(e) {
+          var el = $(e.currentTarget);
+          var index = getSeriesIndexForElement(el);
+          var seriesInfo = seriesList[index];
+          var popoverScope = scope.$new();
+          popoverScope.series = seriesInfo;
+          popoverSrv.show({
+            element: $(':first-child', el),
+            templateUrl:  'app/panels/graph/legend.popover.html',
+            scope: popoverScope
+          });
+        }
+
+        function toggleSeries(e) {
+          var el = $(e.currentTarget);
+          var index = getSeriesIndexForElement(el);
+          var seriesInfo = seriesList[index];
+          scope.toggleSeries(seriesInfo, e);
+        }
+
+        function sortLegend(e) {
+          var el = $(e.currentTarget);
+          var stat = el.data('stat');
+
+          if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
+
+          // if already sort ascending, disable sorting
+          if (panel.legend.sortDesc === false) {
+            panel.legend.sort = null;
+            panel.legend.sortDesc = null;
+            render();
+            return;
+          }
+
+          panel.legend.sortDesc = !panel.legend.sortDesc;
+          panel.legend.sort = stat;
+          render();
+        }
+
+        function getTableHeaderHtml(statName) {
+          if (!panel.legend[statName]) { return ""; }
+          var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
+
+          if (panel.legend.sort === statName) {
+            var cssClass = panel.legend.sortDesc ? 'icon-caret-down' : 'icon-caret-up' ;
+            html += ' <span class="' + cssClass + '"></span>';
+          }
+
+          return html + '</th>';
+        }
+
+        function render() {
+          if (firstRender) {
+            elem.append($container);
+            $container.on('click', '.graph-legend-icon', openColorSelector);
+            $container.on('click', '.graph-legend-alias', toggleSeries);
+            $container.on('click', 'th', sortLegend);
+            firstRender = false;
+          }
+
+          seriesList = data;
+
+          $container.empty();
+
+          $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
+
+          if (panel.legend.alignAsTable) {
+            var header = '<tr>';
+            header += '<th colspan="2" style="text-align:left"></th>';
+            if (panel.legend.values) {
+              header += getTableHeaderHtml('min');
+              header += getTableHeaderHtml('max');
+              header += getTableHeaderHtml('avg');
+              header += getTableHeaderHtml('current');
+              header += getTableHeaderHtml('total');
+            }
+            header += '</tr>';
+            $container.append($(header));
+          }
+
+          if (panel.legend.sort) {
+            seriesList = _.sortBy(seriesList, function(series) {
+              return series.stats[panel.legend.sort];
+            });
+            if (panel.legend.sortDesc) {
+              seriesList = seriesList.reverse();
+            }
+          }
+
+          for (i = 0; i < seriesList.length; i++) {
+            var series = seriesList[i];
+            var html = '<div class="graph-legend-series';
+            if (series.yaxis === 2) { html += ' pull-right'; }
+            if (scope.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
+            html += '" data-series-index="' + i + '">';
+            html += '<div class="graph-legend-icon">';
+            html += '<i class="icon-minus pointer" style="color:' + series.color + '"></i>';
+            html += '</div>';
+
+            html += '<div class="graph-legend-alias">';
+            html += '<a>' + series.label + '</a>';
+            html += '</div>';
+
+            var avg = series.formatValue(series.stats.avg);
+            var current = series.formatValue(series.stats.current);
+            var min = series.formatValue(series.stats.min);
+            var max = series.formatValue(series.stats.max);
+            var total = series.formatValue(series.stats.total);
+
+            if (panel.legend.values) {
+              if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
+              if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
+              if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
+              if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
+              if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
+            }
+
+            html += '</div>';
+            $container.append($(html));
+          }
+        }
+      }
+    };
+  });
+
+});

+ 26 - 0
src/app/panels/graph/legend.popover.html

@@ -0,0 +1,26 @@
+<div class="graph-legend-popover">
+	<a class="close" ng-click="dismiss();" href="">×</a>
+
+	<div class="editor-row small" style="padding-bottom: 0;">
+		<label>Axis:</label>
+		<button ng-click="toggleYAxis(series);dismiss();"
+			class="btn btn-mini"
+			ng-class="{'btn-success': series.yaxis === 1 }">
+			Left
+		</button>
+		<button ng-click="toggleYAxis(series);dismiss();"
+			class="btn btn-mini"
+			ng-class="{'btn-success': series.yaxis === 2 }">
+			Right
+		</button>
+	</div>
+
+	<div class="editor-row">
+		<i ng-repeat="color in colors"
+			class="pointer"
+			ng-class="{'icon-circle-blank': color === series.color,'icon-circle': color !== series.color}"
+			ng-style="{color:color}"
+			ng-click="changeSeriesColor(series, color);dismiss();">&nbsp;</i>
+	</div>
+</div>
+

+ 15 - 18
src/app/panels/graph/module.html

@@ -1,25 +1,22 @@
-<div  ng-controller='GraphCtrl'>
+<div ng-controller='GraphCtrl'>
 
-  <div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
-      <div class="graph-canvas-wrapper">
+	<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
+		<div class="graph-canvas-wrapper">
 
-        <div ng-if="datapointsWarning" class="datapoints-warning">
-          <span class="small" ng-show="!datapointsCount">No datapoints <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
-          <span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
-        </div>
+			<div ng-if="datapointsWarning" class="datapoints-warning">
+				<span class="small" ng-show="!datapointsCount">No datapoints <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
+				<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
+			</div>
 
-        <div grafana-graph class="histogram-chart">
-        </div>
+			<div grafana-graph class="histogram-chart">
+			</div>
 
-      </div>
+		</div>
 
-      <div class="graph-legend-wrapper"
-           ng-if="panel.legend.show"
-           ng-include="'app/panels/graph/legend.html'">
-      </div>
-  </div>
+		<div class="graph-legend-wrapper" ng-if="panel.legend.show" graph-legend></div>
+	</div>
 
-  <div class="clearfix"></div>
+	<div class="clearfix"></div>
 
 	<div style="margin-top: 30px" ng-if="editMode">
 		<div class="dashboard-editor-header">
@@ -29,13 +26,13 @@
 			</div>
 
 			<div ng-model="editor.index" bs-tabs>
-				<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
+				<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
 				</div>
 			</div>
 		</div>
 
 		<div class="dashboard-editor-body">
-			<div ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
+			<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
 				<div ng-include src="tab.src"></div>
 			</div>
 		</div>

+ 75 - 145
src/app/panels/graph/module.js

@@ -6,82 +6,46 @@ define([
   'kbn',
   'moment',
   'components/timeSeries',
-  './seriesOverridesCtrl',
+  'components/panelmeta',
   'services/panelSrv',
   'services/annotationsSrv',
   'services/datasourceSrv',
-  'jquery.flot',
-  'jquery.flot.events',
-  'jquery.flot.selection',
-  'jquery.flot.time',
-  'jquery.flot.stack',
-  'jquery.flot.stackpercent',
-  'jquery.flot.crosshair'
+  './seriesOverridesCtrl',
+  './graph',
+  './legend',
 ],
-function (angular, app, $, _, kbn, moment, TimeSeries) {
+function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
   'use strict';
 
   var module = angular.module('grafana.panels.graph');
-  app.useModule(module);
 
   module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) {
 
-    $scope.panelMeta = {
-      modals : [],
-      editorTabs: [],
-      fullEditorTabs : [
-        {
-          title: 'General',
-          src:'app/partials/panelgeneral.html'
-        },
-        {
-          title: 'Metrics',
-          src:'app/partials/metrics.html'
-        },
-        {
-          title:'Axes & Grid',
-          src:'app/panels/graph/axisEditor.html'
-        },
-        {
-          title:'Display Styles',
-          src:'app/panels/graph/styleEditor.html'
-        }
-      ],
-      fullscreenEdit: true,
-      fullscreenView: true,
-      description : "Graphing"
-    };
+    $scope.panelMeta = new PanelMeta({
+      description: 'Graph panel',
+      fullscreen: true,
+      metricsEditor: true
+    });
+
+    $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
+    $scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
+
+    $scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
+    $scope.panelMeta.addExtendedMenuItem('Toggle legend', '', 'toggleLegend()');
 
     // Set and populate defaults
     var _d = {
-
+      // datasource name, null = default datasource
       datasource: null,
-
-      /** @scratch /panels/histogram/3
-       * renderer:: sets client side (flot) or native graphite png renderer (png)
-       */
+       // sets client side (flot) or native graphite png renderer (png)
       renderer: 'flot',
-      /** @scratch /panels/histogram/3
-       * x-axis:: Show the x-axis
-       */
+       // Show/hide the x-axis
       'x-axis'      : true,
-      /** @scratch /panels/histogram/3
-       * y-axis:: Show the y-axis
-       */
+      // Show/hide y-axis
       'y-axis'      : true,
-      /** @scratch /panels/histogram/3
-       * scale:: Scale the y-axis by this factor
-       */
-      scale         : 1,
-      /** @scratch /panels/histogram/3
-       * y_formats :: 'none','bytes','bits','bps','short', 's', 'ms'
-       */
+      // y axis formats, [left axis,right axis]
       y_formats    : ['short', 'short'],
-      /** @scratch /panels/histogram/5
-       * grid object:: Min and max y-axis values
-       * grid.min::: Minimum y-axis value
-       * grid.ma1::: Maximum y-axis value
-       */
+      // grid options
       grid          : {
         leftMax: null,
         rightMax: null,
@@ -92,48 +56,23 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
         threshold2Color: 'rgba(234, 112, 112, 0.22)'
       },
-
-      annotate      : {
-        enable      : false,
-      },
-
-      /** @scratch /panels/histogram/3
-       * resolution:: If auto_int is true, shoot for this many bars.
-       */
-      resolution    : 100,
-
-      /** @scratch /panels/histogram/3
-       * ==== Drawing options
-       * lines:: Show line chart
-       */
+      // show/hide lines
       lines         : true,
-      /** @scratch /panels/histogram/3
-       * fill:: Area fill factor for line charts, 1-10
-       */
+      // fill factor
       fill          : 0,
-      /** @scratch /panels/histogram/3
-       * linewidth:: Weight of lines in pixels
-       */
+      // line width in pixels
       linewidth     : 1,
-      /** @scratch /panels/histogram/3
-       * points:: Show points on chart
-       */
+      // show hide points
       points        : false,
-      /** @scratch /panels/histogram/3
-       * pointradius:: Size of points in pixels
-       */
+      // point radius in pixels
       pointradius   : 5,
-      /** @scratch /panels/histogram/3
-       * bars:: Show bars on chart
-       */
+      // show hide bars
       bars          : false,
-      /** @scratch /panels/histogram/3
-       * stack:: Stack multiple series
-       */
+      // enable/disable stacking
       stack         : false,
-      /** @scratch /panels/histogram/3
-       * legend:: Display the legend
-       */
+      // stack percentage mode
+      percentage    : false,
+      // legend options
       legend: {
         show: true, // disable/enable legend
         values: false, // disable/enable legend values
@@ -143,31 +82,20 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
         total: false,
         avg: false
       },
-      /** @scratch /panels/histogram/3
-       * ==== Transformations
-      /** @scratch /panels/histogram/3
-       * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
-       * queries
-       */
-      percentage    : false,
-      /** @scratch /panels/histogram/3
-       * zerofill:: Improves the accuracy of line charts at a small performance cost.
-       */
-      zerofill      : true,
-
+      // how null points should be handled
       nullPointMode : 'connected',
-
+      // staircase line mode
       steppedLine: false,
-
+      // tooltip options
       tooltip       : {
         value_type: 'cumulative',
         shared: false,
       },
-
+      // metric queries
       targets: [{}],
-
+      // series color overrides
       aliasColors: {},
-
+      // other style overrides
       seriesOverrides: [],
     };
 
@@ -178,11 +106,17 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
     _.defaults($scope.panel.legend, _d.legend);
 
     $scope.hiddenSeries = {};
+    $scope.seriesList = [];
 
     $scope.updateTimeRange = function () {
       $scope.range = timeSrv.timeRange();
       $scope.rangeUnparsed = timeSrv.timeRange(false);
-      $scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
+      if ($scope.panel.maxDataPoints) {
+        $scope.resolution = $scope.panel.maxDataPoints;
+      }
+      else {
+        $scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12));
+      }
       $scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
     };
 
@@ -206,13 +140,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
           $scope.panelMeta.loading = false;
           $scope.panelMeta.error = err.message || "Timeseries data request error";
           $scope.inspector.error = err;
+          $scope.seriesList = [];
           $scope.render([]);
         });
     };
 
     $scope.dataHandler = function(results) {
       $scope.panelMeta.loading = false;
-      $scope.legend = [];
 
       // png renderer returns just a url
       if (_.isString(results)) {
@@ -224,16 +158,16 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       $scope.datapointsCount = 0;
       $scope.datapointsOutside = false;
 
-      var data = _.map(results.data, $scope.seriesHandler);
+      $scope.seriesList = _.map(results.data, $scope.seriesHandler);
 
       $scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
 
       $scope.annotationsPromise
         .then(function(annotations) {
-          data.annotations = annotations;
-          $scope.render(data);
+          $scope.seriesList.annotations = annotations;
+          $scope.render($scope.seriesList);
         }, function() {
-          $scope.render(data);
+          $scope.render($scope.seriesList);
         });
     };
 
@@ -242,20 +176,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       var alias = seriesData.target;
       var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
 
-      var seriesInfo = {
-        alias: alias,
-        color:  color,
-      };
-
-      $scope.legend.push(seriesInfo);
-
       var series = new TimeSeries({
         datapoints: datapoints,
-        info: seriesInfo,
+        alias: alias,
+        color: color,
       });
 
       if (datapoints && datapoints.length > 0) {
-        var last = moment.utc(datapoints[datapoints.length - 1][1] * 1000);
+        var last = moment.utc(datapoints[datapoints.length - 1][1]);
         var from = moment.utc($scope.range.from);
         if (last - from < -10000) {
           $scope.datapointsOutside = true;
@@ -268,7 +196,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
     };
 
     $scope.render = function(data) {
-      $scope.$emit('render', data);
+      $scope.$broadcast('render', data);
     };
 
     $scope.changeSeriesColor = function(series, color) {
@@ -278,18 +206,18 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
     };
 
     $scope.toggleSeries = function(serie, event) {
-      if ($scope.hiddenSeries[serie.alias]) {
-        delete $scope.hiddenSeries[serie.alias];
-      }
-      else {
-        $scope.hiddenSeries[serie.alias] = true;
-      }
-
       if (event.ctrlKey || event.metaKey || event.shiftKey) {
+        if ($scope.hiddenSeries[serie.alias]) {
+          delete $scope.hiddenSeries[serie.alias];
+        }
+        else {
+          $scope.hiddenSeries[serie.alias] = true;
+        }
+      } else {
         $scope.toggleSeriesExclusiveMode(serie);
       }
 
-      $scope.$emit('toggleLegend', $scope.legend);
+      $scope.render();
     };
 
     $scope.toggleSeriesExclusiveMode = function(serie) {
@@ -300,7 +228,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       }
 
       // check if every other series is hidden
-      var alreadyExclusive = _.every($scope.legend, function(value) {
+      var alreadyExclusive = _.every($scope.seriesList, function(value) {
         if (value.alias === serie.alias) {
           return true;
         }
@@ -310,13 +238,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
 
       if (alreadyExclusive) {
         // remove all hidden series
-        _.each($scope.legend, function(value) {
+        _.each($scope.seriesList, function(value) {
           delete $scope.hiddenSeries[value.alias];
         });
       }
       else {
         // hide all but this serie
-        _.each($scope.legend, function(value) {
+        _.each($scope.seriesList, function(value) {
           if (value.alias === serie.alias) {
             return;
           }
@@ -341,8 +269,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       $scope.render();
     };
 
-    $scope.addSeriesOverride = function() {
-      $scope.panel.seriesOverrides.push({});
+    $scope.addSeriesOverride = function(override) {
+      $scope.panel.seriesOverrides.push(override || {});
     };
 
     $scope.removeSeriesOverride = function(override) {
@@ -350,12 +278,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       $scope.render();
     };
 
-    $scope.toggleEditorHelp = function(index) {
-      if ($scope.editorHelpIndex === index) {
-        $scope.editorHelpIndex = null;
-        return;
-      }
-      $scope.editorHelpIndex = index;
+    // Called from panel menu
+    $scope.toggleLegend = function() {
+      $scope.panel.legend.show = !$scope.panel.legend.show;
+      $scope.get_data();
+    };
+
+    $scope.exportCsv = function() {
+      kbn.exportSeriesListToCsv($scope.seriesList);
     };
 
     panelSrv.init($scope);

+ 12 - 3
src/app/panels/graph/seriesOverridesCtrl.js

@@ -23,7 +23,7 @@ define([
       option.submenu = _.map(values, function(value, index) {
         return {
           text: String(value),
-          click: 'setOverride(' + option.index + ',' + index + ')'
+          click: 'menuItemSelected(' + option.index + ',' + index + ')'
         };
       });
 
@@ -34,6 +34,14 @@ define([
       var option = $scope.overrideMenu[optionIndex];
       var value = option.values[valueIndex];
       $scope.override[option.propertyName] = value;
+
+      // automatically disable lines for this series and the fill bellow to series
+      // can be removed by the user if they still want lines
+      if (option.propertyName === 'fillBelowTo') {
+        $scope.override['lines'] = false;
+        $scope.addSeriesOverride({ alias: value, lines: false });
+      }
+
       $scope.updateCurrentOverrides();
       $scope.render();
     };
@@ -45,8 +53,8 @@ define([
     };
 
     $scope.getSeriesNames = function() {
-      return _.map($scope.legend, function(info) {
-        return info.alias;
+      return _.map($scope.seriesList, function(series) {
+        return series.alias;
       });
     };
 
@@ -67,6 +75,7 @@ define([
     $scope.addOverrideOption('Lines', 'lines', [true, false]);
     $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
     $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
     $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
     $scope.addOverrideOption('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);

+ 3 - 4
src/app/panels/graph/styleEditor.html

@@ -88,11 +88,10 @@
 							<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
 							{{option.name}}: {{option.value}}
 						</li>
-						<li class="dropdown">
-							<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'" data-placement="top">
-								<i class="icon-plus"></i>
-							</a>
+
+						<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($optionIndex, $valueIndex)">
 						</li>
+
 					</ul>
 					<div class="clearfix"></div>
 				</div>

+ 76 - 0
src/app/panels/singlestat/editor.html

@@ -0,0 +1,76 @@
+<div class="editor-row">
+	<div class="section">
+    <h5>Big value</h5>
+		<div class="editor-option">
+			<label class="small">Prefix</label>
+			<input type="text" class="input-small" ng-model="panel.prefix" ng-blur="render()"></input>
+		</div>
+		<div class="editor-option">
+			<label class="small">Value</label>
+			<select class="input-small" ng-model="panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" ng-change="render()"></select>
+		</div>
+		<div class="editor-option">
+			<label class="small">Postfix</label>
+			<input type="text" class="input-small" ng-model="panel.postfix" ng-blur="render()" ng-trim="false"></input>
+		</div>
+	</div>
+
+	<div class="section">
+    <h5>Big value font size</h5>
+		<div class="editor-option">
+			<label class="small">Prefix</label>
+			<select class="input-mini" style="width: 75px;" ng-model="panel.prefixFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%']" ng-change="render()"></select>
+		</div>
+		<div class="editor-option">
+			<label class="small">Value</label>
+			<select class="input-mini" style="width: 75px;" ng-model="panel.valueFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%', '110%', '120%']" ng-change="render()"></select>
+		</div>
+		<div class="editor-option">
+			<label class="small">Postfix</label>
+			<select class="input-mini" style="width: 75px;" ng-model="panel.postfixFontSize" ng-options="f for f in ['30%','50%','70%','80%','100%']" ng-change="render()"></select>
+		</div>
+	</div>
+
+	<div class="section">
+    <h5>Formats</h5>
+		<div class="editor-option">
+			<label class="small">Unit format</label>
+			<select class="input-small" ng-model="panel.format" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
+		</div>
+	</div>
+	<div class="section">
+    <h5>Coloring</h5>
+		<editor-opt-bool text="Background" model="panel.colorBackground" change="setColoring({background: true})"></editor-opt-bool>
+		<editor-opt-bool text="Value" model="panel.colorValue" change="setColoring({value: true})"></editor-opt-bool>
+		<div class="editor-option">
+			<label class="small">Thresholds<tip>Comma seperated values</tip></label>
+			<input type="text" class="input-large" ng-model="panel.thresholds" ng-blur="render()" placeholder="0,50,80"></input>
+		</div>
+		<div class="editor-option">
+      <label class="small">Color</label>
+      <spectrum-picker ng-model="panel.colors[0]" ng-change="render()" ></spectrum-picker>
+      <spectrum-picker ng-model="panel.colors[1]" ng-change="render()" ></spectrum-picker>
+			<spectrum-picker ng-model="panel.colors[2]" ng-change="render()" ></spectrum-picker>
+			<a class="pointer" ng-click="invertColorOrder()">invert order</a>
+		</div>
+	</div>
+</div>
+
+<div class="editor-row">
+	<div class="section">
+		<h5>Spark lines</h5>
+		<editor-opt-bool text="Spark line" model="panel.sparkline.show" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Background mode" model="panel.sparkline.full" change="render()"></editor-opt-bool>
+		<div class="editor-option">
+			<label class="small">Line color</label>
+			<spectrum-picker ng-model="panel.sparkline.lineColor" ng-change="render()" ></spectrum-picker>
+		</div>
+		<div class="editor-option">
+			<label class="small">Fill color</label>
+			<spectrum-picker ng-model="panel.sparkline.fillColor" ng-change="render()" ></spectrum-picker>
+		</div>
+	</div>
+</div>
+
+</div>
+

+ 26 - 0
src/app/panels/singlestat/module.html

@@ -0,0 +1,26 @@
+<div ng-controller='SingleStatCtrl'>
+
+	<div class="singlestat-panel" singlestat-panel></div>
+
+  <div class="clearfix"></div>
+
+	<div style="margin-top: 30px" ng-if="editMode">
+		<div class="dashboard-editor-header">
+			<div class="dashboard-editor-title">
+				<i class="icon icon-dashboard"></i>
+			  Singlestat
+			</div>
+
+			<div ng-model="editor.index" bs-tabs>
+				<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
+				</div>
+			</div>
+		</div>
+
+		<div class="dashboard-editor-body">
+			<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
+				<div ng-include src="tab.src"></div>
+			</div>
+		</div>
+	</div>
+</div>

+ 197 - 0
src/app/panels/singlestat/module.js

@@ -0,0 +1,197 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'components/timeSeries',
+  'kbn',
+  'components/panelmeta',
+  'services/panelSrv',
+  './singleStatPanel',
+],
+function (angular, app, _, TimeSeries, kbn, PanelMeta) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.singlestat');
+  app.useModule(module);
+
+  module.controller('SingleStatCtrl', function($scope, panelSrv, timeSrv) {
+
+    $scope.panelMeta = new PanelMeta({
+      description: 'Singlestat panel',
+      titlePos: 'left',
+      fullscreen: true,
+      metricsEditor: true
+    });
+
+    $scope.panelMeta.addEditorTab('Options', 'app/panels/singlestat/editor.html');
+
+    // Set and populate defaults
+    var _d = {
+      links: [],
+      maxDataPoints: 100,
+      interval: null,
+      targets: [{}],
+      cacheTimeout: null,
+      format: 'none',
+      prefix: '',
+      postfix: '',
+      valueName: 'avg',
+      prefixFontSize: '50%',
+      valueFontSize: '100%',
+      postfixFontSize: '50%',
+      thresholds: '',
+      colorBackground: false,
+      colorValue: false,
+      colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+      sparkline: {
+        show: false,
+        full: false,
+        lineColor: 'rgb(31, 120, 193)',
+        fillColor: 'rgba(31, 118, 189, 0.18)',
+      }
+    };
+
+    _.defaults($scope.panel, _d);
+
+    $scope.init = function() {
+      panelSrv.init($scope);
+      $scope.$on('refresh', $scope.get_data);
+    };
+
+    $scope.updateTimeRange = function () {
+      $scope.range = timeSrv.timeRange();
+      $scope.rangeUnparsed = timeSrv.timeRange(false);
+      $scope.resolution = $scope.panel.maxDataPoints;
+      $scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
+    };
+
+    $scope.get_data = function() {
+      $scope.updateTimeRange();
+
+      var metricsQuery = {
+        range: $scope.rangeUnparsed,
+        interval: $scope.interval,
+        targets: $scope.panel.targets,
+        maxDataPoints: $scope.resolution,
+        cacheTimeout: $scope.panel.cacheTimeout
+      };
+
+      return $scope.datasource.query(metricsQuery)
+        .then($scope.dataHandler)
+        .then(null, function(err) {
+          console.log("err");
+          $scope.panelMeta.loading = false;
+          $scope.panelMeta.error = err.message || "Timeseries data request error";
+          $scope.inspector.error = err;
+          $scope.render();
+        });
+    };
+
+    $scope.dataHandler = function(results) {
+      $scope.panelMeta.loading = false;
+      $scope.series = _.map(results.data, $scope.seriesHandler);
+      $scope.render();
+    };
+
+    $scope.seriesHandler = function(seriesData) {
+      var series = new TimeSeries({
+        datapoints: seriesData.datapoints,
+        alias: seriesData.target,
+      });
+
+      series.flotpairs = series.getFlotPairs('connected');
+
+      return series;
+    };
+
+    $scope.setColoring = function(options) {
+      if (options.background) {
+        $scope.panel.colorValue = false;
+        $scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
+      }
+      else {
+        $scope.panel.colorBackground = false;
+        $scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
+      }
+      $scope.render();
+    };
+
+    $scope.invertColorOrder = function() {
+      var tmp = $scope.panel.colors[0];
+      $scope.panel.colors[0] = $scope.panel.colors[2];
+      $scope.panel.colors[2] = tmp;
+      $scope.render();
+    };
+
+    $scope.getDecimalsForValue = function(value) {
+      var opts = {};
+      if (value === 0) {
+        return { decimals: 0, scaledDecimals: 0 };
+      }
+
+      var delta = value / 2;
+      var dec = -Math.floor(Math.log(delta) / Math.LN10);
+
+      var magn = Math.pow(10, -dec),
+          norm = delta / magn, // norm is between 1.0 and 10.0
+          size;
+
+      if (norm < 1.5) {
+        size = 1;
+      } else if (norm < 3) {
+        size = 2;
+        // special case for 2.5, requires an extra decimal
+        if (norm > 2.25) {
+          size = 2.5;
+          ++dec;
+        }
+      } else if (norm < 7.5) {
+        size = 5;
+      } else {
+        size = 10;
+      }
+
+      size *= magn;
+
+      if (opts.minTickSize != null && size < opts.minTickSize) {
+        size = opts.minTickSize;
+      }
+
+      var result = {};
+      result.decimals = Math.max(0, dec);
+      result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN11) + 2;
+
+      return result;
+    };
+
+    $scope.render = function() {
+      var data = {};
+
+      if (!$scope.series || $scope.series.length === 0) {
+        data.flotpairs = [];
+        data.mainValue = Number.NaN;
+        data.mainValueFormated = 'NaN';
+      }
+      else {
+        var series = $scope.series[0];
+        data.mainValue = series.stats[$scope.panel.valueName];
+        var decimalInfo = $scope.getDecimalsForValue(data.mainValue);
+        var formatFunc = kbn.valueFormats[$scope.panel.format];
+
+        data.mainValueFormated = formatFunc(data.mainValue, decimalInfo.decimals, decimalInfo.scaledDecimals);
+        data.flotpairs = series.flotpairs;
+      }
+
+      data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
+        return Number(strVale.trim());
+      });
+
+      data.colorMap = $scope.panel.colors;
+
+      $scope.data = data;
+      $scope.$emit('render');
+    };
+
+    $scope.init();
+  });
+});

+ 204 - 0
src/app/panels/singlestat/singleStatPanel.js

@@ -0,0 +1,204 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+  'jquery.flot',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.singlestat', []);
+  app.useModule(module);
+
+  module.directive('singlestatPanel', function($location, linkSrv, $timeout) {
+
+    return {
+      link: function(scope, elem) {
+        var data, panel;
+        var $panelContainer = elem.parents('.panel-container');
+
+        scope.$on('render', function() {
+          render();
+        });
+
+        function setElementHeight() {
+          try {
+            var height = scope.height || panel.height || scope.row.height;
+            if (_.isString(height)) {
+              height = parseInt(height.replace('px', ''), 10);
+            }
+
+            height -= panel.title ? 24 : 9; // subtract panel title bar
+
+            elem.css('height', height + 'px');
+
+            return true;
+          } catch(e) { // IE throws errors sometimes
+            return false;
+          }
+        }
+
+        function applyColoringThresholds(value, valueString) {
+          if (!panel.colorValue) {
+            return valueString;
+          }
+
+          var color = getColorForValue(value);
+          if (color) {
+            return '<span style="color:' + color + '">'+ valueString + '</span>';
+          }
+
+          return valueString;
+        }
+
+        function getColorForValue(value) {
+          for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
+            if (value > data.thresholds[i]) {
+              return data.colorMap[i];
+            }
+          }
+          return null;
+        }
+
+        function getSpan(className, fontSize, value)  {
+          return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
+            value + '</span>';
+        }
+
+        function getBigValueHtml() {
+          var body = '<div class="singlestat-panel-value-container">';
+
+          if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
+
+          var value = applyColoringThresholds(data.mainValue, data.mainValueFormated);
+          body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
+
+          if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
+
+          body += '</div>';
+
+          return body;
+        }
+
+        function addSparkline() {
+          var panel = scope.panel;
+          var width = elem.width() + 20;
+          var height = elem.height() || 100;
+
+          var plotCanvas = $('<div></div>');
+          var plotCss = {};
+          plotCss.position = 'absolute';
+
+          if (panel.sparkline.full) {
+            plotCss.bottom = '5px';
+            plotCss.left = '-5px';
+            plotCss.width = (width - 10) + 'px';
+            plotCss.height = (height - 45) + 'px';
+          }
+          else {
+            plotCss.bottom = "0px";
+            plotCss.left = "-5px";
+            plotCss.width = (width - 10) + 'px';
+            plotCss.height = Math.floor(height * 0.25) + "px";
+          }
+
+          plotCanvas.css(plotCss);
+
+          var options = {
+            legend: { show: false },
+            series: {
+              lines:  {
+                show: true,
+                fill: 1,
+                lineWidth: 1,
+                fillColor: panel.sparkline.fillColor,
+              },
+            },
+            yaxes: { show: false },
+            xaxis: {
+              show: false,
+              mode: "time",
+              min: scope.range.from.getTime(),
+              max: scope.range.to.getTime(),
+            },
+            grid: { hoverable: false, show: false },
+          };
+
+          elem.append(plotCanvas);
+
+          var plotSeries = {
+            data: data.flotpairs,
+            color: panel.sparkline.lineColor
+          };
+
+          setTimeout(function() {
+            $.plot(plotCanvas, [plotSeries], options);
+          }, 10);
+        }
+
+        function render() {
+          if (!scope.data) { return; }
+
+          data = scope.data;
+          panel = scope.panel;
+
+          setElementHeight();
+
+          var body = getBigValueHtml();
+
+          if (panel.colorBackground && data.mainValue) {
+            var color = getColorForValue(data.mainValue);
+            if (color) {
+              $panelContainer.css('background-color', color);
+              if (scope.fullscreen) {
+                elem.css('background-color', color);
+              } else {
+                elem.css('background-color', '');
+              }
+            }
+          } else {
+            $panelContainer.css('background-color', '');
+            elem.css('background-color', '');
+          }
+
+          elem.html(body);
+
+          if (panel.sparkline.show) {
+            addSparkline();
+          }
+
+          elem.toggleClass('pointer', panel.links.length > 0);
+        }
+
+        // drilldown link tooltip
+        var drilldownTooltip = $('<div id="tooltip" class="">gello</div>"');
+
+        elem.mouseleave(function() {
+          if (panel.links.length === 0) { return;}
+          drilldownTooltip.detach();
+        });
+
+        elem.click(function() {
+          if (panel.links.length === 0) { return; }
+
+          var linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0]);
+          if (linkInfo.href[0] === '#') { linkInfo.href = linkInfo.href.substring(1); }
+
+          $timeout(function() { $location.url(linkInfo.href); });
+
+          drilldownTooltip.detach();
+        });
+
+        elem.mousemove(function(e) {
+          if (panel.links.length === 0) { return;}
+
+          drilldownTooltip.text('click to go to: ' + panel.links[0].title);
+
+          drilldownTooltip.place_tt(e.clientX+20, e.clientY-15);
+        });
+      }
+    };
+  });
+
+});

+ 8 - 5
src/app/panels/text/module.js

@@ -3,8 +3,9 @@ define([
   'app',
   'lodash',
   'require',
+  'components/panelmeta',
 ],
-function (angular, app, _, require) {
+function (angular, app, _, require, PanelMeta) {
   'use strict';
 
   var module = angular.module('grafana.panels.text', []);
@@ -14,13 +15,15 @@ function (angular, app, _, require) {
 
   module.controller('text', function($scope, templateSrv, $sce, panelSrv) {
 
-    $scope.panelMeta = {
+    $scope.panelMeta = new PanelMeta({
       description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
-    };
+    });
+
+    $scope.panelMeta.addEditorTab('Edit text', 'app/panels/text/editor.html');
 
     // Set and populate defaults
     var _d = {
-      title: 'default title',
+      title   : 'default title',
       mode    : "markdown", // 'html', 'markdown', 'text'
       content : "",
       style: {},
@@ -29,7 +32,7 @@ function (angular, app, _, require) {
     _.defaults($scope.panel, _d);
 
     $scope.init = function() {
-      panelSrv.init(this);
+      panelSrv.init($scope);
       $scope.ready = false;
       $scope.$on('refresh', $scope.render);
       $scope.render();

+ 18 - 11
src/app/panels/timepicker/module.html

@@ -10,19 +10,26 @@
     }
   </style>
   <form name="input" style="margin:0">
-    <ul class="nav nav-pills timepicker-dropdown">
-      <li class="dropdown">
+		<ul class="nav timepicker-dropdown">
 
-        <a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
-          <span ng-bind="time.rangeString"></span>
-          <span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
-          <i class="icon-caret-down"></i>
-        </a>
+			<li class="grafana-menu-zoom-out">
+				<a class='small' ng-click='zoom(2)'>
+					Zoom Out
+				</a>
+			</li>
 
-        <ul class="dropdown-menu">
-          <!-- Relative time options -->
-          <li bindonce ng-repeat='timespan in panel.time_options track by $index'>
-            <a ng-click="setRelativeFilter(timespan)" bo-text="'Last ' + timespan"></a>
+			<li class="dropdown">
+
+				<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
+					<span ng-bind="time.rangeString"></span>
+					<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
+					<i class="icon-caret-down"></i>
+				</a>
+
+				<ul class="dropdown-menu">
+					<!-- Relative time options -->
+					<li bindonce ng-repeat='timespan in panel.time_options track by $index'>
+						<a ng-click="setRelativeFilter(timespan)" bo-text="'Last ' + timespan"></a>
           </li>
 
           <!-- Auto refresh submenu -->

+ 2 - 2
src/app/panels/timepicker/module.js

@@ -75,8 +75,8 @@ function (angular, app, _, moment, kbn) {
 
       // Date picker needs the date to be at the start of the day
       if(new Date().getTimezoneOffset() < 0) {
-        $scope.temptime.from.date = moment($scope.temptime.from.date).add('days',1).toDate();
-        $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
+        $scope.temptime.from.date = moment($scope.temptime.from.date).add(1, 'days').toDate();
+        $scope.temptime.to.date = moment($scope.temptime.to.date).add(1, 'days').toDate();
       }
 
       $scope.appEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });

+ 23 - 0
src/app/partials/confirm_modal.html

@@ -0,0 +1,23 @@
+<div class="modal-body">
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-ok"></i>
+			{{title}}
+		</div>
+	</div>
+
+	<div class="dashboard-editor-body">
+		<p class="row-fluid text-center large">
+			{{text}}
+			<br>
+			<br>
+		</p>
+		<div class="row-fluid">
+		<span class="span4"></span>
+	  <button type="button" class="btn btn-success span2" ng-click="dismiss()">No</button>
+	  <button type="button" class="btn btn-danger span2" ng-click="onConfirm();dismiss();">Yes</button>
+		<span class="span4"></span>
+  </div>
+
+</div>
+

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

@@ -14,7 +14,7 @@
 	<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-control-inner">
 					<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)">
@@ -77,29 +77,25 @@
 				<div class="panels-wrapper" 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="panel-menu-container" data-menu-container>
-							<!-- <a class="pointer"><i class="icon&#45;eye&#45;open"></i> <span>view</span></a> -->
-							<!-- <a class="pointer"><i class="icon&#45;cog"></i> <span>edit</span></a> -->
-							<!-- <a class="pointer"><i class="icon&#45;resize&#45;horizontal"></i> <span>span</span></a> -->
-							<!-- <a class="pointer"><i class="icon&#45;copy"></i> <span>duplicate</span></a> -->
-							<!-- <a class="pointer"><i class="icon&#45;share"></i> <span>share</span></a> -->
-							<!-- <a class="pointer"><i class="icon&#45;remove"></i> <span>remove</span></a> -->
-					</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}">
+						class="panel"
+						ui-draggable="true" drag="panel.id"
+						ui-on-Drop="onDrop($data, row, panel)"
+						drag-handle-class="drag-handle" panel-width ng-model="panel">
+
 						<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 panel-drop-zone class="panel panel-drop-zone"
+							 ui-on-drop="onDrop($data, row)"
+							 data-drop="true">
+							 <div class="panel-container" style="background: transparent">
+								 <div style="text-align: center">
+									 <em>Drop here</em>
+								 </div>
+							 </div>
 					</div>
 
 					<div class="clearfix"></div>

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

@@ -13,12 +13,6 @@
 					</a>
 				</li>
 
-				<li class="grafana-menu-zoom-out">
-					<a class='small' ng-click='zoom(2)'>
-						Zoom Out
-					</a>
-				</li>
-
 				<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
 					<grafana-simple-panel type="pulldown.type" ng-cloak>
 					</grafana-simple-panel>

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

@@ -84,7 +84,7 @@
 		</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>
+			<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
 			<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
 		</div>
 

+ 51 - 9
src/app/partials/graphite/editor.html

@@ -30,6 +30,19 @@
                   ng-click="duplicate()">
                 Duplicate
               </a>
+            </li>
+						<li role="menuitem">
+              <a  tabindex="1"
+                  ng-click="moveMetricQuery($index, $index-1)">
+                Move up
+              </a>
+            </li>
+						<li role="menuitem">
+							<a  tabindex="1"
+                  ng-click="moveMetricQuery($index, $index+1)">
+                Move down
+              </a>
+            </li>
           </ul>
         </li>
         <li>
@@ -83,16 +96,29 @@
 					<i class="icon-wrench"></i>
 				</li>
 				<li class="grafana-target-segment">
-					cacheTimeout
+					Cache timeout
 				</li>
 				<li>
 					<input type="text"
-					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">
+								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 class="grafana-target-segment">
+					Max data points
+				</li>
+				<li>
+					<input type="text"
+								class="input-mini grafana-target-segment-input"
+								ng-model="panel.maxDataPoints"
+								bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
+								data-placement="right"
+								ng-model-onblur ng-change="get_data()"
+								spellcheck='false'
+								placeholder="auto">
 				</li>
 			</ul>
 			<div class="clearfix"></div>
@@ -122,6 +148,11 @@
 						templating
 					</a>
 				</li>
+				<li class="grafana-target-segment">
+					<a ng-click="toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
+						max data points
+					</a>
+				</li>
 			</ul>
 			<div class="clearfix"></div>
 		</div>
@@ -177,7 +208,18 @@
 			</ul>
 		</div>
 
+		<div class="grafana-info-box span6" ng-if="editorHelpIndex === 5">
+			<h5>Max data points</h5>
+			<ul>
+				<li>Every graphite request is issued with a maxDataPoints parameter</li>
+				<li>Graphite uses this parameter to consolidate the real number of values down to this number</li>
+				<li>If there are more real values, then by default they will be consolidated using averages</li>
+				<li>This could hide real peaks and max values in your series</li>
+				<li>You can change how point consolidation is made using the consolidateBy graphite function</li>
+				<li>Point consolidation will effect series legend values (min,max,total,current)</li>
+				<li>If you override maxDataPoint and set a high value performance can be severely effected</li>
+			</ul>
+		</div>
+
 	</div>
 </div>
-
-

+ 50 - 0
src/app/partials/help_modal.html

@@ -0,0 +1,50 @@
+<div class="modal-body">
+	<div class="dashboard-editor-header">
+		<div class="dashboard-editor-title">
+			<i class="icon icon-keyboard"></i>
+			Keyboard shutcuts
+		</div>
+	</div>
+
+	<div class="dashboard-editor-body">
+		<table class="shortcut-table">
+			<tr>
+				<th></th>
+				<th style="text-align: left;">Dashboard wide shortcuts</th>
+			</tr>
+			<tr>
+				<td style="text-align: right;"><span class="label label-info">ESC</span></td>
+				<td>Exit fullscreen edit/view mode, close search or any editor view</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+F</span></td>
+				<td>Open dashboard search view (also contains import/playlist controls)</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+S</span></td>
+				<td>Save dashboard</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+H</span></td>
+				<td>Hide row controls</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+Z</span></td>
+				<td>Zoom out</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+R</span></td>
+				<td>Refresh (Fetches new data and rerenders panels)</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">CTRL+O</span></td>
+				<td>Enable/Disable shared graph crosshair</td>
+			</tr>
+		</table>
+	</div>
+
+</div>
+
+<div class="modal-footer">
+	<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
+</div>

+ 21 - 19
src/app/partials/influxdb/editor.html

@@ -15,26 +15,26 @@
                tabindex="1">
               <i class="icon icon-cog"></i>
             </a>
-            <ul class="dropdown-menu pull-right" role="menu">
-              <li role="menuitem">
-                <a tabindex="1" ng-click="duplicate()">Duplicate</a>
-                <a tabindex="2" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a>
-                <a tabindex="2" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a>
-              </li>
-           </ul>
-          </li>
-          <li>
-            <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
-              <i class="icon icon-remove"></i>
-            </a>
-          </li>
-        </ul>
+						<ul class="dropdown-menu pull-right" role="menu">
+							<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
+							<li role="menuitem"><a tabindex="1" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a></li>
+							<li role="menuitem"><a tabindex="1" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a></li>
+							<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up </a></li>
+							<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
+						</ul>
+					</li>
+					<li>
+						<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
+							<i class="icon icon-remove"></i>
+						</a>
+					</li>
+				</ul>
 
-        <ul class="grafana-segment-list">
-          <li>
-            <a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
-              <i class="icon-eye-open"></i>
-            </a>
+				<ul class="grafana-segment-list">
+					<li>
+						<a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
+							<i class="icon-eye-open"></i>
+						</a>
           </li>
         </ul>
 
@@ -64,6 +64,8 @@
                    ng-model="target.series"
                    spellcheck='false'
                    bs-typeahead="listSeries"
+                   match-all="true"
+                   min-length="3"
                    placeholder="series name"
                    data-min-length=0 data-items=100
                    ng-blur="seriesBlur()">

+ 1 - 0
src/app/partials/metrics.html

@@ -14,4 +14,5 @@
     </ul>
   </div>
 
+	<div class="clearfix"></div>
 </div>

+ 26 - 0
src/app/partials/opentsdb/editor.html

@@ -89,6 +89,32 @@
                    ng-model="target.isCounter"
                    ng-change="targetBlur()">
           </li>
+          <li class="grafana-target-segment" ng-hide="!target.isCounter">
+            Counter Max:
+          </li>
+          <li ng-hide="!target.isCounter">
+            <input type="text"
+                   class="grafana-target-segment-input input-medium"
+                   ng-disabled="!target.shouldComputeRate"
+                   ng-model="target.counterMax"
+                   spellcheck='false'
+                   placeholder="Counter max value"
+                   ng-blur="targetBlur()"
+                   />
+          </li>
+          <li class="grafana-target-segment" ng-hide="!target.isCounter">
+            Counter Reset Value:
+          </li>
+          <li ng-hide="!target.isCounter">
+            <input type="text"
+                   class="grafana-target-segment-input input-medium"
+                   ng-disabled="!target.shouldComputeRate"
+                   ng-model="target.counterResetValue"
+                   spellcheck='false'
+                   placeholder="Counter reset value"
+                   ng-blur="targetBlur()"
+                   />
+          </li>
           <li class="grafana-target-segment">
             Alias:
           </li>

+ 2 - 10
src/app/partials/paneleditor.html

@@ -5,22 +5,14 @@
 	</div>
 
 	<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
-		<div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
+		<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
 		</div>
 	</div>
 
 </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-repeat="tab in panelMeta.editorTabs" ng-show="editor.index == $index">
 		<div ng-include src="tab.src"></div>
 	</div>
 </div>

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

@@ -12,3 +12,7 @@
     </div>
   </div>
 </div>
+
+<panel-link-editor panel="panel"></panel-link-editor>
+
+

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

@@ -22,7 +22,7 @@
 				</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()" />
+									ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" 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>

+ 12 - 5
src/app/routes/dashboard-from-script.js

@@ -16,21 +16,28 @@ function (angular, $, config, _, kbn, moment) {
       .when('/dashboard/script/:jsFile', {
         templateUrl: 'app/partials/dashboard.html',
         controller : 'DashFromScriptProvider',
+        reloadOnSearch: false,
       });
   });
 
-  module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, alertSrv, $q) {
+  module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, $q, dashboardSrv, datasourceSrv, $timeout) {
 
     var execute_script = function(result) {
+      var services = {
+        dashboardSrv: dashboardSrv,
+        datasourceSrv: datasourceSrv,
+        $q: $q,
+      };
+
       /*jshint -W054 */
-      var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data);
-      var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $);
+      var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', 'services', result.data);
+      var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $, services);
 
       // Handle async dashboard scripts
       if (_.isFunction(script_result)) {
         var deferred = $q.defer();
         script_result(function(dashboard) {
-          $rootScope.$apply(function() {
+          $timeout(function() {
             deferred.resolve({ data: dashboard });
           });
         });
@@ -47,7 +54,7 @@ function (angular, $, config, _, kbn, moment) {
       .then(execute_script)
       .then(null,function(err) {
         console.log('Script dashboard error '+ err);
-        alertSrv.set('Error', "Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard", 'error');
+        $scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
         return false;
       });
     };

+ 25 - 1
src/app/services/alertSrv.js

@@ -7,7 +7,7 @@ function (angular, _) {
 
   var module = angular.module('grafana.services');
 
-  module.service('alertSrv', function($timeout, $sce, $rootScope) {
+  module.service('alertSrv', function($timeout, $sce, $rootScope, $modal, $q) {
     var self = this;
 
     this.init = function() {
@@ -20,6 +20,7 @@ function (angular, _) {
       $rootScope.onAppEvent('alert-success', function(e, alert) {
         self.set(alert[0], alert[1], 'success', 3000);
       });
+      $rootScope.onAppEvent('confirm-modal', this.showConfirmModal);
     };
 
     // List of all alert objects
@@ -57,5 +58,28 @@ function (angular, _) {
     this.clearAll = function() {
       self.list = [];
     };
+
+    this.showConfirmModal = function(e, payload) {
+      var scope = $rootScope.$new();
+
+      scope.title = payload.title;
+      scope.text = payload.text;
+      scope.onConfirm = payload.onConfirm;
+
+      var confirmModal = $modal({
+        template: './app/partials/confirm_modal.html',
+        persist: true,
+        modalClass: 'confirm-modal',
+        show: false,
+        scope: scope,
+        keyboard: false
+      });
+
+      $q.when(confirmModal).then(function(modalEl) {
+        modalEl.modal('show');
+      });
+
+    };
+
   });
 });

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

@@ -7,9 +7,9 @@ define([
   './templateValuesSrv',
   './panelSrv',
   './timer',
-  './panelMove',
   './keyboardManager',
   './annotationsSrv',
+  './popoverSrv',
   './playlistSrv',
   './unsavedChangesSrv',
   './dashboard/dashboardKeyBindings',

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

@@ -58,7 +58,7 @@ define([
 
     function errorHandler(err) {
       console.log('Annotation error: ', err);
-      var message = err.message || "Aannotation query failed";
+      var message = err.message || "Annotation query failed";
       alertSrv.set('Annotations error', message,'error');
     }
 

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

@@ -1,14 +1,13 @@
 define([
   'angular',
   'jquery',
-  'services/all'
 ],
 function(angular, $) {
   "use strict";
 
   var module = angular.module('grafana.services');
 
-  module.service('dashboardKeybindings', function($rootScope, keyboardManager) {
+  module.service('dashboardKeybindings', function($rootScope, keyboardManager, $modal, $q) {
 
     this.shortcuts = function(scope) {
 
@@ -22,6 +21,24 @@ function(angular, $) {
         keyboardManager.unbind('esc');
       });
 
+      var helpModalScope = null;
+      keyboardManager.bind('shift+?', function() {
+        if (helpModalScope) { return; }
+
+        helpModalScope = $rootScope.$new();
+        var helpModal = $modal({
+          template: './app/partials/help_modal.html',
+          persist: false,
+          show: false,
+          scope: helpModalScope,
+          keyboard: false
+        });
+
+        helpModalScope.$on('$destroy', function() { helpModalScope = null; });
+        $q.when(helpModal).then(function(modalEl) { modalEl.modal('show'); });
+
+      }, { inputDisabled: true });
+
       keyboardManager.bind('ctrl+f', function() {
         scope.appEvent('show-dash-editor', { src: 'app/partials/search.html' });
       }, { inputDisabled: true });
@@ -32,6 +49,10 @@ function(angular, $) {
         scope.dashboard.emit_refresh('refresh');
       }, { inputDisabled: true });
 
+      keyboardManager.bind('ctrl+l', function() {
+        scope.$broadcast('toggle-all-legends');
+      }, { inputDisabled: true });
+
       keyboardManager.bind('ctrl+h', function() {
         var current = scope.dashboard.hideControls;
         scope.dashboard.hideControls = !current;

+ 21 - 0
src/app/services/dashboard/dashboardSrv.js

@@ -35,6 +35,7 @@ function (angular, $, kbn, _, moment) {
       this.annotations = this._ensureListExist(data.annotations);
       this.refresh = data.refresh;
       this.version = data.version || 0;
+      this.hideAllLegends = data.hideAllLegends || false;
 
       if (this.nav.length === 0) {
         this.nav.push({ type: 'timepicker' });
@@ -91,6 +92,26 @@ function (angular, $, kbn, _, moment) {
       row.panels.push(panel);
     };
 
+    p.getPanelInfoById = function(panelId) {
+      var result = {};
+      _.each(this.rows, function(row) {
+        _.each(row.panels, function(panel, index) {
+          if (panel.id === panelId) {
+            result.panel = panel;
+            result.row = row;
+            result.index = index;
+            return;
+          }
+        });
+      });
+
+      if (!result.panel) {
+        return null;
+      }
+
+      return result;
+    };
+
     p.duplicatePanel = function(panel, row) {
       var rowIndex = _.indexOf(this.rows, row);
       var newPanel = angular.copy(panel);

+ 17 - 15
src/app/services/dashboard/dashboardViewStateSrv.js

@@ -32,33 +32,34 @@ function (angular, _, $) {
       });
 
       this.update(this.getQueryStringState(), true);
+      this.expandRowForPanel();
     }
 
+    DashboardViewState.prototype.expandRowForPanel = function() {
+      if (!this.state.panelId) { return; }
+
+      var panelInfo = this.$scope.dashboard.getPanelInfoById(this.state.panelId);
+      if (panelInfo) {
+        panelInfo.row.collapse = false;
+      }
+    };
+
     DashboardViewState.prototype.needsSync = function(urlState) {
       return _.isEqual(this.state, urlState) === false;
     };
 
     DashboardViewState.prototype.getQueryStringState = function() {
-      var queryParams = $location.search();
-      var urlState = {
-        panelId: parseInt(queryParams.panelId) || null,
-        fullscreen: queryParams.fullscreen ? true : false,
-        edit: queryParams.edit ? true : false,
-      };
-
-      _.each(queryParams, function(value, key) {
-        if (key.indexOf('var-') !== 0) { return; }
-        urlState[key] = value;
-      });
-
-      return urlState;
+      var state = $location.search();
+      state.panelId = parseInt(state.panelId) || null;
+      state.fullscreen = state.fullscreen ? true : null;
+      state.edit =  (state.edit === "true" || state.edit === true) || null;
+      return state;
     };
 
     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;
     };
 
@@ -68,7 +69,8 @@ function (angular, _, $) {
 
       if (!this.state.fullscreen) {
         this.state.panelId = null;
-        this.state.edit = false;
+        this.state.fullscreen = null;
+        this.state.edit = null;
       }
 
       if (!skipUrlSync) {

+ 11 - 9
src/app/services/elasticsearch/es-datasource.js

@@ -224,7 +224,7 @@ function (angular, _, config, kbn, moment) {
       var endsInOpen = function(string, opener, closer) {
         var character;
         var count = 0;
-        for (var i=0; i<string.length; i++) {
+        for (var i = 0, len = string.length; i < len; i++) {
           character = string[i];
 
           if (character === opener) {
@@ -279,18 +279,20 @@ function (angular, _, config, kbn, moment) {
             return { dashboards: [], tags: [] };
           }
 
-          var hits = { dashboards: [], tags: results.facets.tags.terms || [] };
+          var resultsHits = results.hits.hits;
+          var displayHits = { dashboards: [], tags: results.facets.tags.terms || [] };
 
-          for (var i = 0; i < results.hits.hits.length; i++) {
-            hits.dashboards.push({
-              id: results.hits.hits[i]._id,
-              title: results.hits.hits[i]._source.title,
-              tags: results.hits.hits[i]._source.tags
+          for (var i = 0, len = resultsHits.length; i < len; i++) {
+            var hit = resultsHits[i];
+            displayHits.dashboards.push({
+              id: hit._id,
+              title: hit._source.title,
+              tags: hit._source.tags
             });
           }
 
-          hits.tagsOnly = tagsOnly;
-          return hits;
+          displayHits.tagsOnly = tagsOnly;
+          return displayHits;
         });
     };
 

+ 40 - 5
src/app/services/graphite/gfunc.js

@@ -39,6 +39,13 @@ function (_) {
     defaultParams: [1],
   });
 
+  addFuncDef({
+    name: 'perSecond',
+    category: categories.Transform,
+    params: [],
+    defaultParams: [],
+  });
+
   addFuncDef({
     name: "holtWintersForecast",
     category: categories.Calculate,
@@ -93,6 +100,27 @@ function (_) {
     category: categories.Combine,
   });
 
+  addFuncDef({
+    name: 'mapSeries',
+    shortName: 'map',
+    params: [{ name: "node", type: 'int' }],
+    defaultParams: [3],
+    category: categories.Combine,
+  });
+
+  addFuncDef({
+    name: 'reduceSeries',
+    shortName: 'reduce',
+    params: [
+      { name: "function", type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
+      { name: "reduceNode", type: 'int', options: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] },
+      { name: "reduceMatchers", type: 'string' },
+      { name: "reduceMatchers", type: 'string' },
+    ],
+    defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
+    category: categories.Combine,
+  });
+
   addFuncDef({
     name: 'sumSeries',
     shortName: 'sum',
@@ -148,7 +176,10 @@ function (_) {
   addFuncDef({
     name: 'averageSeriesWithWildcards',
     category: categories.Combine,
-    params: [{ name: "node", type: "int" }],
+    params: [
+      { name: "node", type: "int" },
+      { name: "node", type: "int", optional: true },
+    ],
     defaultParams: [3]
   });
 
@@ -193,7 +224,7 @@ function (_) {
       {
         name: "node",
         type: "int",
-        options: [1,2,3,4,5,6,7,8,9,10,12]
+        options: [0,1,2,3,4,5,6,7,8,9,10,12]
       },
       {
         name: "function",
@@ -329,8 +360,12 @@ function (_) {
   addFuncDef({
     name: 'summarize',
     category: categories.Transform,
-    params: [{ name: "interval", type: "string" }, { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] }],
-    defaultParams: ['1h', 'sum']
+    params: [
+      { name: "interval", type: "string" },
+      { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] },
+      { name: "alignToFrom", type: "boolean", optional: true, options: ['false', 'true'] },
+    ],
+    defaultParams: ['1h', 'sum', 'false']
   });
 
   addFuncDef({
@@ -533,7 +568,7 @@ function (_) {
     var parameters = _.map(this.params, function(value, index) {
 
       var paramType = this.def.params[index].type;
-      if (paramType === 'int' || paramType === 'value_or_series') {
+      if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
         return value;
       }
 

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

@@ -53,13 +53,24 @@ function (angular, _, $, config, kbn, moment) {
           httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
         }
 
-        return this.doGraphiteRequest(httpOptions);
+        return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
       }
       catch(err) {
         return $q.reject(err);
       }
     };
 
+    GraphiteDatasource.prototype.convertDataPointsToMs = function(result) {
+      if (!result || !result.data) { return []; }
+      for (var i = 0; i < result.data.length; i++) {
+        var series = result.data[i];
+        for (var y = 0; y < series.datapoints.length; y++) {
+          series.datapoints[y][1] *= 1000;
+        }
+      }
+      return result;
+    };
+
     GraphiteDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
       // Graphite metric as annotation
       if (annotation.target) {
@@ -84,7 +95,7 @@ function (angular, _, $, config, kbn, moment) {
 
                 list.push({
                   annotation: annotation,
-                  time: datapoint[1] * 1000,
+                  time: datapoint[1],
                   title: target.target
                 });
               }

+ 1 - 1
src/app/services/influxdb/influxQueryBuilder.js

@@ -18,7 +18,7 @@ function () {
     var query = 'select ';
     var seriesName = target.series;
 
-    if(!seriesName.match('^/.*/')) {
+    if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) {
       seriesName = '"' + seriesName+ '"';
     }
 

+ 8 - 4
src/app/services/influxdb/influxdbDatasource.js

@@ -44,7 +44,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
 
         // replace grafana variables
         query = query.replace('$timeFilter', timeFilter);
-        query = query.replace('$interval', (target.interval || options.interval));
+        query = query.replace(/\$interval/g, (target.interval || options.interval));
 
         // replace templated variables
         query = templateSrv.replace(query);
@@ -85,8 +85,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       });
     };
 
-    InfluxDatasource.prototype.listSeries = function() {
-      return this._seriesQuery('list series').then(function(data) {
+    InfluxDatasource.prototype.listSeries = function(query) {
+      // wrap in regex
+      if (query && query.length > 0 && query[0] !== '/')  {
+        query = '/' + query + '/';
+      }
+
+      return this._seriesQuery('list series ' + query).then(function(data) {
         if (!data || data.length === 0) {
           return [];
         }
@@ -141,7 +146,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
     InfluxDatasource.prototype._seriesQuery = function(query) {
       return this._influxRequest('GET', '/series', {
         q: query,
-        time_precision: 's',
       });
     };
 

+ 5 - 1
src/app/services/keyboardManager.js

@@ -61,6 +61,7 @@ function (angular) {
         else if (e.which) {
           code = e.which;
         }
+
         var character = String.fromCharCode(code).toLowerCase();
 
         if (code === 188) {
@@ -93,6 +94,9 @@ function (angular) {
           ",": "<",
           ".": ">",
           "/": "?",
+          "»": "?",
+          "«": "?",
+          "¿": "?",
           "\\": "|"
         };
         // Special Keys - and their codes
@@ -277,4 +281,4 @@ function (angular) {
     return keyboardManagerService;
   }]);
 
-});
+});

+ 19 - 5
src/app/services/opentsdb/opentsdbDatasource.js

@@ -1,14 +1,15 @@
 define([
   'angular',
   'lodash',
-  'kbn'
+  'kbn',
+  'moment'
 ],
 function (angular, _, kbn) {
   'use strict';
 
   var module = angular.module('grafana.services');
 
-  module.factory('OpenTSDBDatasource', function($q, $http) {
+  module.factory('OpenTSDBDatasource', function($q, $http, templateSrv) {
 
     function OpenTSDBDatasource(datasource) {
       this.type = 'opentsdb';
@@ -99,7 +100,7 @@ function (angular, _, kbn) {
       // TSDB returns datapoints has a hash of ts => value.
       // Can't use _.pairs(invert()) because it stringifies keys/values
       _.each(md.dps, function (v, k) {
-        dps.push([v, k]);
+        dps.push([v, k * 1000]);
       });
 
       return { target: metricLabel, datapoints: dps };
@@ -123,12 +124,12 @@ function (angular, _, kbn) {
       }
 
       var query = {
-        metric: target.metric,
+        metric: templateSrv.replace(target.metric),
         aggregator: "avg"
       };
 
       if (target.aggregator) {
-        query.aggregator = target.aggregator;
+        query.aggregator = templateSrv.replace(target.aggregator);
       }
 
       if (target.shouldComputeRate) {
@@ -136,6 +137,14 @@ function (angular, _, kbn) {
         query.rateOptions = {
           counter: !!target.isCounter
         };
+
+        if (target.counterMax && target.counterMax.length) {
+          query.rateOptions.counterMax = parseInt(target.counterMax);
+        }
+
+        if (target.counterResetValue && target.counterResetValue.length) {
+          query.rateOptions.resetValue = parseInt(target.counterResetValue);
+        }
       }
 
       if (target.shouldDownsample) {
@@ -143,6 +152,11 @@ function (angular, _, kbn) {
       }
 
       query.tags = angular.copy(target.tags);
+      if(query.tags){
+        for(var key in query.tags){
+          query.tags[key] = templateSrv.replace(query.tags[key]);
+        }
+      }
 
       return query;
     }

+ 0 - 85
src/app/services/panelMove.js

@@ -1,85 +0,0 @@
-define([
-  'angular',
-  'lodash'
-],
-function (angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.service('panelMoveSrv', function($rootScope) {
-
-    function PanelMoveSrv(dashboard) {
-      this.dashboard = dashboard;
-      _.bindAll(this, 'onStart', 'onOver', 'onOut', 'onDrop', 'onStop', 'cleanup');
-    }
-
-    var p = PanelMoveSrv.prototype;
-
-    /* each of these can take event,ui,data parameters */
-    p.onStart = function() {
-      this.dashboard.$$panelDragging =  true;
-      $rootScope.$apply();
-    };
-
-    p.onOver = function() {
-      $rootScope.$apply();
-    };
-
-    p.onOut = function() {
-      $rootScope.$apply();
-    };
-
-    /*
-      Use our own drop logic. the $parent.$parent this is ugly.
-    */
-    p.onDrop = function(event,ui,data) {
-      var
-        dragRow = data.draggableScope.$parent.$parent.row.panels,
-        dropRow =  data.droppableScope.$parent.$parent.row.panels,
-        dragIndex = data.dragSettings.index,
-        dropIndex =  data.dropSettings.index;
-
-      // Remove panel from source row
-      dragRow.splice(dragIndex,1);
-
-      // Add to destination row
-      if (!_.isUndefined(dropRow)) {
-        dropRow.splice(dropIndex,0,data.dragItem);
-      }
-
-      this.dashboard.$$panelDragging = false;
-      // Cleanup nulls/undefined left behind
-      this.cleanup();
-      $rootScope.$apply();
-      $rootScope.$broadcast('render');
-    };
-
-    p.onStop = function() {
-      this.dashboard.$$panelDragging = false;
-      this.cleanup();
-      $rootScope.$apply();
-    };
-
-    p.cleanup = function () {
-      _.each(this.dashboard.rows, function(row) {
-        row.panels = _.without(row.panels,{});
-        row.panels = _.compact(row.panels);
-      });
-    };
-
-    return {
-      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;
-      }
-    };
-
-  });
-
-});

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

@@ -11,44 +11,10 @@ function (angular, _) {
     this.init = function($scope) {
       if (!$scope.panel.span) { $scope.panel.span = 12; }
 
-      var menu = [
-        {
-          text: "view",
-          icon: "icon-eye-open",
-          click: 'toggleFullscreen(false)',
-          condition: $scope.panelMeta.fullscreenView
-        },
-        {
-          text: 'edit',
-          icon: 'icon-cogs',
-          click: 'editPanel()',
-          condition: true,
-        },
-        {
-          text: 'duplicate',
-          icon: 'icon-copy',
-          click: 'duplicatePanel(panel)',
-          condition: true
-        },
-        {
-          text: 'json',
-          icon: 'icon-code',
-          click: 'editPanelJson()',
-          condition: true
-        },
-        {
-          text: 'share',
-          icon: 'icon-share',
-          click: 'sharePanel()',
-          condition: true
-        },
-      ];
-
       $scope.inspector = {};
-      $scope.panelMeta.menu = _.where(menu, { condition: true });
 
       $scope.editPanel = function() {
-        if ($scope.panelMeta.fullscreenEdit) {
+        if ($scope.panelMeta.fullscreen) {
           $scope.toggleFullscreen(true);
         }
         else {
@@ -67,6 +33,10 @@ function (angular, _) {
         $scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
       };
 
+      $scope.duplicatePanel = function() {
+        $scope.dashboard.duplicatePanel($scope.panel, $scope.row);
+      };
+
       $scope.updateColumnSpan = function(span) {
         $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12);
 
@@ -99,6 +69,14 @@ function (angular, _) {
         $scope.get_data();
       };
 
+      $scope.toggleEditorHelp = function(index) {
+        if ($scope.editorHelpIndex === index) {
+          $scope.editorHelpIndex = null;
+          return;
+        }
+        $scope.editorHelpIndex = index;
+      };
+
       $scope.toggleFullscreen = function(edit) {
         $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
       };
@@ -110,9 +88,6 @@ function (angular, _) {
       // Post init phase
       $scope.fullscreen = false;
       $scope.editor = { index: 1 };
-      if ($scope.panelMeta.fullEditorTabs) {
-        $scope.editorTabs = _.pluck($scope.panelMeta.fullEditorTabs, 'title');
-      }
 
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.setDatasource($scope.panel.datasource);

+ 46 - 0
src/app/services/popoverSrv.js

@@ -0,0 +1,46 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('popoverSrv', function($templateCache, $timeout, $q, $http, $compile) {
+
+    this.getTemplate = function(url) {
+      return $q.when($templateCache.get(url) || $http.get(url, {cache: true}));
+    };
+
+    this.show = function(options) {
+      var popover = options.element.data('popover');
+      if (popover) {
+        popover.scope.$destroy();
+        popover.destroy();
+        return;
+      }
+
+      this.getTemplate(options.templateUrl).then(function(result) {
+        var template = _.isString(result) ? result : result.data;
+
+        options.element.popover({
+          content: template,
+          placement: 'bottom',
+          html: true
+        });
+
+        popover = options.element.data('popover');
+        popover.hasContent = function () {
+          return template;
+        };
+
+        popover.toggle();
+        popover.scope = options.scope;
+        $compile(popover.$tip)(popover.scope);
+      });
+    };
+
+  });
+
+});

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

@@ -30,6 +30,7 @@ function (angular, _, kbn) {
           var option = _.findWhere(variable.options, { text: urlValue });
           option = option || { text: urlValue, value: urlValue };
           this.setVariableValue(variable, option, true);
+          this.updateAutoInterval(variable);
         }
         else if (variable.refresh) {
           this.updateOptions(variable);

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

@@ -95,6 +95,18 @@ define([
       $timeout(this.refreshDashboard, 0);
     };
 
+    this.timeRangeForUrl = function() {
+      var range = this.timeRange(false);
+      if (_.isString(range.to) && range.to.indexOf('now')) {
+        range = this.timeRange();
+      }
+
+      if (_.isDate(range.from)) { range.from = range.from.getTime(); }
+      if (_.isDate(range.to)) { range.to = range.to.getTime(); }
+
+      return range;
+    };
+
     this.timeRange = function(parse) {
       var _t = this.time;
       if(_.isUndefined(_t) || _.isUndefined(_t.from)) {

+ 1 - 1
src/config.sample.js

@@ -73,7 +73,7 @@ define(['settings'], function(Settings) {
 
       // specify the limit for dashboard search results
       search: {
-        max_results: 20
+        max_results: 100
       },
 
       // default home dashboard

+ 23 - 3
src/css/less/grafana.less

@@ -7,10 +7,17 @@
 @import "search.less";
 @import "panel.less";
 @import "forms.less";
+@import "singlestat.less";
+
+.row-control-inner {
+  padding:0px;
+  margin:0px;
+  position:relative;
+}
 
 .hide-controls {
   padding: 0;
-  .row-control-inner {
+  .row-tab {
     display: none;
   }
   .submenu-controls {
@@ -110,7 +117,7 @@
   position: fixed;
   left: 0px;
   right: 0px;
-  top: 54px;
+  top: 51px;
   height: 100%;
   padding: 0 10px;
   background: @grafanaPanelBackground;
@@ -128,8 +135,11 @@
 
 .dashboard-fullscreen {
   .main-view-container {
-    height: 0;
     overflow: hidden;
+    height: 0;
+    .row-control-inner {
+      display: none;
+    }
   }
 }
 
@@ -546,3 +556,13 @@ select.grafana-target-segment-input {
 .grafana-tip {
   padding-left: 5px;
 }
+
+.shortcut-table {
+  td { padding: 3px; }
+  th:last-child { text-align: left; }
+  td:first-child { text-align: right; }
+}
+
+.confirm-modal {
+  max-width: 500px;
+}

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

@@ -18,12 +18,12 @@
   top: 1px;
 }
 
-.graph-legend-series,
 .graph-legend-icon,
 .graph-legend-alias,
 .graph-legend-value {
   float: left;
   white-space: nowrap;
+  font-size: 85%;
   text-align: left;
   &.current:before {
     content: "Current: "
@@ -43,6 +43,8 @@
 }
 
 .graph-legend-series {
+  float: left;
+  white-space: nowrap;
   padding-left: 10px;
   padding-top: 6px;
 }
@@ -53,6 +55,8 @@
 
 .graph-legend-table {
   display: table;
+  width: 100%;
+  margin: 0;
 
   .graph-legend-series {
     display: table-row;
@@ -60,34 +64,59 @@
     padding-left: 0;
     &.pull-right {
       float: none;
-      .graph-legend-alias::after {
-        content: 'y\00B2';
-      }
     }
   }
 
-  .graph-legend-alias {
+  td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value {
     float: none;
     display: table-cell;
     white-space: nowrap;
+    padding: 2px 10px;
+    text-align: right;
+    border-bottom: 1px solid @grafanaListBorderBottom;
   }
 
   .graph-legend-icon {
-    display: table-cell;
-    float: none;
-    white-space: nowrap;
-    padding: 0 4px;
-    top: 2px;
+    width: 5px;
+    padding: 0;
+    top: 0;
+    .icon-minus {
+      position: relative;
+      top: 2px;
+    }
   }
 
-  .graph-legend-value  {
-    float: none;
-    display: table-cell;
-    white-space: nowrap;
+ .graph-legend-value  {
     padding-left: 15px;
   }
-}
 
+  .graph-legend-alias {
+    padding-left: 7px;
+    text-align: left;
+    width: 95%;
+  }
+
+  .graph-legend-series:nth-child(odd) {
+    background-color: @grafanaListAccent;
+  }
+
+  .graph-legend-value {
+    &.current, &.max, &.min, &.total, &.avg {
+      &:before {
+        content: '';
+      }
+    }
+  }
+
+  th {
+    text-align: right;
+    padding: 5px 10px;
+    font-weight: bold;
+    color: @blue;
+    font-size: 85%;
+    white-space: nowrap;
+  }
+}
 
 .graph-legend-rightside {
 
@@ -106,7 +135,8 @@
     display: table-cell;
     vertical-align: top;
     position: relative;
-    left: -4px;
+    left: 4px;
+    top: -20px;
   }
 
   .graph-legend {
@@ -169,6 +199,8 @@
 }
 
 .graph-tooltip {
+  white-space: nowrap;
+
   .graph-tooltip-time {
     text-align: center;
     font-weight: bold;
@@ -176,9 +208,18 @@
     top: -3px;
   }
 
+  .graph-tooltip-list-item {
+    display: table-row;
+  }
+
+  .graph-tooltip-series-name {
+    display: table-cell;
+  }
+
   .graph-tooltip-value {
+    display: table-cell;
     font-weight: bold;
-    float: right;
     padding-left: 10px;
+    text-align: right;
   }
 }

+ 0 - 4
src/css/less/overrides.less

@@ -231,10 +231,6 @@ form input.ng-invalid {
   z-index: 9999;
 }
 
-.dragInProgress .panel-container {
-  border: 3px solid rgba(100,100,100,0.50);
-}
-
 .link {
   color: @linkColor;
   cursor: pointer;

+ 28 - 2
src/css/less/panel.less

@@ -2,6 +2,7 @@
   display: inline-block;
   float: left;
   vertical-align: top;
+  position: relative;
 }
 
 .panel-container {
@@ -17,6 +18,7 @@
 
 .panel-title-container {
   min-height: 5px;
+  padding-top: 4px;
   cursor: context-menu;
 }
 
@@ -24,8 +26,16 @@
   border: 0px;
   font-weight: bold;
   position: relative;
-  font-size: 0.9em;
   cursor: context-menu;
+
+  &.has-panel-links {
+    .panel-title-text:after {
+      content: "\f0c1";
+      font-family:'FontAwesome';
+      font-size: 80%;
+      padding-left: 10px;
+    }
+  }
 }
 
 .panel-loading {
@@ -39,7 +49,6 @@
   text-align: center;
 }
 
-
 .panel-error {
   color: @white;
   position: absolute;
@@ -93,8 +102,25 @@
       border: none;
     }
   }
+
+  .dropdown-menu {
+    text-align: left;
+  }
 }
 
 .panel-highlight  {
   .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236, 0.8)");
 }
+
+.on-drag-hover {
+  .panel-container {
+    .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236, 0.8)");
+  }
+}
+
+.panel-drop-zone {
+  display: none;
+  .panel-container {
+    border: 1px solid @grayDark;
+  }
+}

+ 51 - 0
src/css/less/singlestat.less

@@ -0,0 +1,51 @@
+.singlestat-panel {
+  position: relative;
+  display: table;
+  width: 100%;
+}
+
+.singlestat-panel-value-container {
+  padding: 20px;
+  display: table-cell;
+  vertical-align: middle;
+  text-align: center;
+  position: relative;
+  z-index: 1;
+  font-size: 3em;
+  font-weight: bold;
+}
+
+.singlestat-panel-prefix {
+  padding-right: 20px;
+}
+
+.singlestat-panel-table {
+  width: 100%;
+  td {
+    padding: 5px 10px;
+    white-space: nowrap;
+    text-align: right;
+    border-bottom: 1px solid @grafanaListBorderBottom;
+  }
+
+  th {
+    text-align: right;
+    padding: 5px 10px;
+    font-weight: bold;
+    color: @blue
+  }
+
+  td:first-child {
+    text-align: left;
+  }
+
+  tr:nth-child(odd) td {
+    background-color: @grafanaListAccent;
+  }
+
+  tr:last-child td {
+    border: none;
+  }
+}
+
+

+ 1 - 1
src/css/less/submenu.less

@@ -1,6 +1,6 @@
 .submenu-controls-visible:not(.hide-controls) {
   .panel-fullscreen {
-    top: 91px;
+    top: 88px;
   }
 }
 

+ 10 - 0
src/plugins/custom.panel.example/editor.html

@@ -0,0 +1,10 @@
+<div>
+  <div class="row-fluid">
+    <div class="span4">
+      <label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
+    </div>
+    <div class="span2" ng-show="panel.mode == 'text'">
+      <label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
+    </div>
+  </div>
+</div>

+ 3 - 0
src/plugins/custom.panel.example/module.html

@@ -0,0 +1,3 @@
+<div ng-controller='CustomPanelCtrl'>
+	<h2>Custom panel</h2>
+</div>

+ 31 - 0
src/plugins/custom.panel.example/module.js

@@ -0,0 +1,31 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'require',
+],
+function (angular, app, _) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.custom', []);
+  app.useModule(module);
+
+  module.controller('CustomPanelCtrl', function($scope, panelSrv) {
+
+    $scope.panelMeta = {
+      description : "Example plugin panel",
+    };
+
+    // set and populate defaults
+    var _d = {
+    };
+
+    _.defaults($scope.panel, _d);
+
+    $scope.init = function() {
+      panelSrv.init($scope);
+    };
+
+    $scope.init();
+  });
+});

+ 1 - 1
src/plugins/datasource.example.js

@@ -19,7 +19,7 @@ function (angular, _, kbn) {
       this.url = datasource.url;
     }
 
-    CustomDatasource.prototype.query = function(filterSrv, options) {
+    CustomDatasource.prototype.query = function(options) {
       // get from & to in seconds
       var from = kbn.parseDate(options.range.from).getTime() / 1000;
       var to = kbn.parseDate(options.range.to).getTime() / 1000;

+ 1 - 1
src/test/specs/dashboardViewStateSrv-specs.js

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

+ 1 - 1
src/test/specs/gfunc-specs.js

@@ -79,7 +79,7 @@ define([
       var func = gfunc.createFuncInstance('summarize', { withDefaultParams: true });
       func.updateParam('1h', 0);
       expect(func.params[0]).to.be('1h');
-      expect(func.text).to.be('summarize(1h, sum)');
+      expect(func.text).to.be('summarize(1h, sum, false)');
     });
 
     it('should parse numbers as float', function() {

+ 10 - 5
src/test/specs/graph-ctrl-specs.js

@@ -27,15 +27,20 @@ define([
         ctx.scope.$digest();
       });
 
-      it('should build legend model', function() {
-        expect(ctx.scope.legend[0].alias).to.be('test.cpu1');
-        expect(ctx.scope.legend[1].alias).to.be('test.cpu2');
-      });
-
       it('should send time series to render', function() {
         var data = ctx.scope.render.getCall(0).args[0];
         expect(data.length).to.be(2);
       });
+
+      describe('get_data failure following success', function() {
+        beforeEach(function() {
+          ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
+          ctx.scope.get_data();
+          ctx.scope.$digest();
+        });
+
+      });
+
     });
 
   });

+ 21 - 7
src/test/specs/grafanaGraph-specs.js → src/test/specs/graph-specs.js

@@ -3,7 +3,7 @@ define([
   'angular',
   'jquery',
   'components/timeSeries',
-  'directives/grafanaGraph'
+  'panels/graph/graph'
 ], function(helpers, angular, $, TimeSeries) {
   'use strict';
 
@@ -47,11 +47,11 @@ define([
             ctx.data = [];
             ctx.data.push(new TimeSeries({
               datapoints: [[1,1],[2,2]],
-              info: { alias: 'series1', enable: true }
+              alias: 'series1'
             }));
             ctx.data.push(new TimeSeries({
               datapoints: [[1,1],[2,2]],
-              info: { alias: 'series2', enable: true }
+              alias: 'series2'
             }));
 
             setupFunc(scope, ctx.data);
@@ -126,6 +126,20 @@ define([
       });
     });
 
+    graphScenario('should use timeStep for barWidth', function(ctx) {
+      ctx.setup(function(scope, data) {
+        scope.panel.bars = true;
+        data[0] = new TimeSeries({
+          datapoints: [[1,10],[2,20]],
+          alias: 'series1',
+        });
+      });
+
+      it('should set barWidth', function() {
+        expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
+      });
+    });
+
     graphScenario('series option overrides, fill & points', function(ctx) {
       ctx.setup(function(scope, data) {
         scope.panel.lines = true;
@@ -134,7 +148,7 @@ define([
           { alias: 'test', fill: 0, points: true }
         ];
 
-        data[1].info.alias = 'test';
+        data[1].alias = 'test';
       });
 
       it('should match second series and fill zero, and enable points', function() {
@@ -150,8 +164,8 @@ define([
       });
 
       it('should move zindex 2 last', function() {
-        expect(ctx.plotData[0].info.alias).to.be('series2');
-        expect(ctx.plotData[1].info.alias).to.be('series1');
+        expect(ctx.plotData[0].alias).to.be('series2');
+        expect(ctx.plotData[1].alias).to.be('series1');
       });
     });
 
@@ -161,7 +175,7 @@ define([
       });
 
       it('should remove datapoints and disable stack', function() {
-        expect(ctx.plotData[0].info.alias).to.be('series1');
+        expect(ctx.plotData[0].alias).to.be('series1');
         expect(ctx.plotData[1].data.length).to.be(0);
         expect(ctx.plotData[1].stack).to.be(false);
       });

+ 79 - 41
src/test/specs/graph-tooltip-specs.js

@@ -1,55 +1,93 @@
 define([
   'jquery',
-  'directives/grafanaGraph.tooltip'
-], function($, tooltip) {
+  'panels/graph/graph.tooltip'
+], function($, GraphTooltip) {
   'use strict';
 
-  describe('graph tooltip', function() {
-    var elem = $('<div></div>');
-    var dashboard = {
-      formatDate: sinon.stub().returns('date'),
-    };
-    var scope =  {
-      appEvent: sinon.spy(),
-      onAppEvent: sinon.spy(),
-      panel: {
-        tooltip:  {
-          shared: true
-        },
-        y_formats: ['ms', 'none'],
-        stack: true
-      }
-    };
+  var scope =  {
+    appEvent: sinon.spy(),
+    onAppEvent: sinon.spy(),
+  };
+
+  var elem = $('<div></div>');
+  var dashboard = { };
 
-    var data = [
-      {
-        data: [[10,10], [12,20]],
-        info: { yaxis: 1 },
-        yaxis: { tickDecimals: 2 },
+  function describeSharedTooltip(desc, fn) {
+    var ctx = {};
+    ctx.scope = scope;
+    ctx.scope.panel =  {
+      tooltip:  {
+        shared: true
       },
-      {
-        data: [[10,10], [12,20]],
-        info: { yaxis: 1 },
-        yaxis: { tickDecimals: 2 },
-      }
-    ];
-
-    var plot = {
-      getData: sinon.stub().returns(data),
-      highlight: sinon.stub(),
-      unhighlight: sinon.stub()
+      stack: false
+    };
+
+    ctx.setup = function(setupFn) {
+      ctx.setupFn = setupFn;
     };
 
-    elem.data('plot', plot);
+    describe(desc, function() {
+      beforeEach(function() {
+        ctx.setupFn();
+        var tooltip = new GraphTooltip(elem, dashboard, scope);
+        ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
+      });
+
+      fn(ctx);
+    });
+  }
+
+  describeSharedTooltip("steppedLine false, stack false", function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [
+        { data: [[10, 15], [12, 20]], },
+        { data: [[10, 2], [12, 3]], }
+      ];
+      ctx.pos = { x: 11 };
+    });
+
+    it('should return 2 series', function() {
+      expect(ctx.results.length).to.be(2);
+    });
+    it('should add time to results array', function() {
+      expect(ctx.results.time).to.be(10);
+    });
+    it('should set value and hoverIndex', function() {
+      expect(ctx.results[0].value).to.be(15);
+      expect(ctx.results[1].value).to.be(2);
+      expect(ctx.results[0].hoverIndex).to.be(0);
+    });
+  });
+
+  describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [
+        { data: [[10, 15], [12, 20]], },
+        { data: [[10, 2], [12, 3]], }
+      ];
+      ctx.scope.panel.stack = true;
+      ctx.pos = { x: 11 };
+    });
+
+    it('should show stacked value', function() {
+      expect(ctx.results[1].value).to.be(17);
+    });
+
+  });
 
-    beforeEach(function() {
-      tooltip.register(elem, dashboard, scope);
-      elem.trigger('plothover', [{}, {x: 13}, {}]);
+  describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [
+        { data: [[10, 15], [12, 20]], },
+        { data: [[10, 2], [12, 3]], }
+      ];
+      ctx.scope.panel.stack = true;
+      ctx.scope.panel.tooltip.value_type = 'individual';
+      ctx.pos = { x: 11 };
     });
 
-    it('should add tooltip', function() {
-      var tooltipHtml = $(".graph-tooltip").text();
-      expect(tooltipHtml).to.be('date  : 40.00 ms : 20.00 ms');
+    it('should not show stacked value', function() {
+      expect(ctx.results[1].value).to.be(2);
     });
 
   });

+ 1 - 1
src/test/specs/graphiteDatasource-specs.js

@@ -21,7 +21,7 @@ define([
         maxDataPoints: 500,
       };
 
-      var response = [{ target: 'prod1.count', points: [[10, 1], [12,1]], }];
+      var response = [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]], }];
       var results;
       var request;
 

+ 29 - 0
src/test/specs/influxQueryBuilder-specs.js

@@ -44,6 +44,35 @@ define([
 
     });
 
+    describe('merge function detection', function() {
+      it('should not quote wrap regex merged series', function() {
+        var builder = new InfluxQueryBuilder({
+          series: 'merge(/^google.test/)',
+          column: 'value',
+          function: 'mean'
+        });
+
+        var query = builder.build();
+
+        expect(query).to.be('select mean(value) from merge(/^google.test/) where $timeFilter ' +
+          'group by time($interval) order asc');
+      });
+
+      it('should quote wrap series names that start with "merge"', function() {
+        var builder = new InfluxQueryBuilder({
+          series: 'merge.google.test',
+          column: 'value',
+          function: 'mean'
+        });
+
+        var query = builder.build();
+
+        expect(query).to.be('select mean(value) from "merge.google.test" where $timeFilter ' +
+          'group by time($interval) order asc'); 
+      });
+
+    });
+
   });
 
 });

+ 3 - 3
src/test/specs/influxdb-datasource-specs.js

@@ -17,7 +17,7 @@ define([
     describe('When querying influxdb with one target using query editor target spec', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+
-                        "+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc&time_precision=s";
+                        "+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc";
       var query = {
         range: { from: 'now-1h', to: 'now' },
         targets: [{ series: 'test', column: 'value', function: 'mean' }],
@@ -50,7 +50,7 @@ define([
     describe('When querying influxdb with one raw query', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+value+from+series"+
-                        "+where+time+%3E+now()+-+1h&time_precision=s";
+                        "+where+time+%3E+now()+-+1h";
       var query = {
         range: { from: 'now-1h', to: 'now' },
         targets: [{ query: "select value from series where $timeFilter", rawQuery: true }]
@@ -73,7 +73,7 @@ define([
     describe('When issuing annotation query', function() {
       var results;
       var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
-                        "+where+time+%3E+now()+-+1h&time_precision=s";
+                        "+where+time+%3E+now()+-+1h";
 
       var range = { from: 'now-1h', to: 'now' };
       var annotation = { query: 'select title from events.$server where $timeFilter' };

+ 1 - 1
src/test/specs/seriesOverridesCtrl-specs.js

@@ -18,7 +18,7 @@ define([
 
     describe('Controller should init overrideMenu', function() {
       it('click should include option and value index', function() {
-        expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('setOverride(1,1)');
+        expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('menuItemSelected(1,1)');
       });
     });
 

+ 12 - 13
src/test/specs/sharePanelCtrl-specs.js

@@ -7,6 +7,12 @@ define([
   describe('SharePanelCtrl', function() {
     var ctx = new helpers.ControllerTestContext();
 
+    function setTime(range) {
+      ctx.timeSrv.timeRangeForUrl = sinon.stub().returns(range);
+    }
+
+    setTime({ from: 'now-1h', to: 'now' });
+
     beforeEach(module('grafana.controllers'));
 
     beforeEach(ctx.providePhase());
@@ -14,10 +20,12 @@ define([
 
     describe('shareUrl with current time range and panel', function() {
 
+
       it('should generate share url relative time', function() {
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
-        ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
+
+        setTime({ from: 'now-1h', to: 'now' });
 
         ctx.scope.buildUrl();
         expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&panelId=22&fullscreen');
@@ -26,26 +34,17 @@ define([
       it('should generate share url absolute time', function() {
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
-        ctx.timeSrv.time = { from: new Date(1362178800000), to: new Date(1396648800000) };
+        setTime({ from: 1362178800000, to: 1396648800000 });
 
         ctx.scope.buildUrl();
         expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1362178800000&to=1396648800000&panelId=22&fullscreen');
       });
 
-      it('should generate share url with time as JSON strings', function() {
-        ctx.$location.path('/test');
-        ctx.scope.panel = { id: 22 };
-        ctx.timeSrv.time = { from: "2012-01-31T23:00:00.000Z", to: "2014-04-04T22:00:00.000Z" };
-
-        ctx.scope.buildUrl();
-        expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1328050800000&to=1396648800000&panelId=22&fullscreen');
-      });
-
       it('should remove panel id when toPanel is false', function() {
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
         ctx.scope.toPanel = false;
-        ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
+        setTime({ from: 'now-1h', to: 'now' });
 
         ctx.scope.buildUrl();
         expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now');
@@ -57,7 +56,7 @@ define([
         ctx.scope.includeTemplateVars = true;
         ctx.scope.toPanel = false;
         ctx.templateSrv.variables = [{ name: 'app', current: {text: 'mupp' }}, {name: 'server', current: {text: 'srv-01'}}];
-        ctx.timeSrv.time = { from: 'now-1h', to: 'now' };
+        setTime({ from: 'now-1h', to: 'now' });
 
         ctx.scope.buildUrl();
         expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&var-app=mupp&var-server=srv-01');

+ 28 - 8
src/test/specs/timeSeries-specs.js

@@ -7,7 +7,7 @@ define([
     var points, series;
     var yAxisFormats = ['short', 'ms'];
     var testData = {
-      info: { alias: 'test' },
+      alias: 'test',
       datapoints: [
         [1,2],[null,3],[10,4],[8,5]
       ]
@@ -26,6 +26,15 @@ define([
         expect(points.length).to.be(4);
         expect(points[1][1]).to.be(0);
       });
+
+      it('if last is null current should pick next to last', function() {
+        series = new TimeSeries({
+          datapoints: [[10,1], [null, 2]]
+        });
+        series.getFlotPairs('null', yAxisFormats);
+        expect(series.stats.current).to.be(10);
+      });
+
     });
 
     describe('series overrides', function() {
@@ -36,7 +45,7 @@ define([
 
       describe('fill & points', function() {
         beforeEach(function() {
-          series.info.alias = 'test';
+          series.alias = 'test';
           series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
         });
 
@@ -48,7 +57,7 @@ define([
 
       describe('series option overrides, bars, true & lines false', function() {
         beforeEach(function() {
-          series.info.alias = 'test';
+          series.alias = 'test';
           series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
         });
 
@@ -60,7 +69,7 @@ define([
 
       describe('series option overrides, linewidth, stack', function() {
         beforeEach(function() {
-          series.info.alias = 'test';
+          series.alias = 'test';
           series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
         });
 
@@ -70,9 +79,20 @@ define([
         });
       });
 
+      describe('series option overrides, fill below to', function() {
+        beforeEach(function() {
+          series.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
+        });
+
+        it('should disable line fill and add fillBelowTo', function() {
+          expect(series.fillBelowTo).to.be('min');
+        });
+      });
+
       describe('series option overrides, pointradius, steppedLine', function() {
         beforeEach(function() {
-          series.info.alias = 'test';
+          series.alias = 'test';
           series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
         });
 
@@ -84,7 +104,7 @@ define([
 
       describe('override match on regex', function() {
         beforeEach(function() {
-          series.info.alias = 'test_01';
+          series.alias = 'test_01';
           series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
         });
 
@@ -95,12 +115,12 @@ define([
 
       describe('override series y-axis, and z-index', function() {
         beforeEach(function() {
-          series.info.alias = 'test';
+          series.alias = 'test';
           series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
         });
 
         it('should set yaxis', function() {
-          expect(series.info.yaxis).to.be(2);
+          expect(series.yaxis).to.be(2);
         });
 
         it('should set zindex', function() {

+ 4 - 5
src/test/test-main.js

@@ -32,8 +32,6 @@ require.config({
     bootstrap:                '../vendor/bootstrap/bootstrap',
     'bootstrap-tagsinput':    '../vendor/tagsinput/bootstrap-tagsinput',
 
-    'jquery-ui':              '../vendor/jquery/jquery-ui-1.10.3',
-
     'extend-jquery':          'components/extend-jquery',
 
     'jquery.flot':            '../vendor/jquery/jquery.flot',
@@ -44,6 +42,7 @@ require.config({
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
+    'jquery.flot.fillbelow':  '../vendor/jquery/jquery.flot.fillbelow',
 
     modernizr:                '../vendor/modernizr-2.6.1',
   },
@@ -70,7 +69,6 @@ require.config({
       exports: 'Crypto'
     },
 
-    'jquery-ui':            ['jquery'],
     'jquery.flot':          ['jquery'],
     'jquery.flot.pie':      ['jquery', 'jquery.flot'],
     'jquery.flot.events':   ['jquery', 'jquery.flot'],
@@ -79,10 +77,11 @@ require.config({
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+    'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
 
     'angular-route':        ['angular'],
     'angular-cookies':      ['angular'],
-    'angular-dragdrop':     ['jquery','jquery-ui','angular'],
+    'angular-dragdrop':     ['jquery', 'angular'],
     'angular-loader':       ['angular'],
     'angular-mocks':        ['angular'],
     'angular-resource':     ['angular'],
@@ -130,7 +129,7 @@ require([
     'specs/influxQueryBuilder-specs',
     'specs/influxdb-datasource-specs',
     'specs/graph-ctrl-specs',
-    'specs/grafanaGraph-specs',
+    'specs/graph-specs',
     'specs/graph-tooltip-specs',
     'specs/seriesOverridesCtrl-specs',
     'specs/sharePanelCtrl-specs',

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