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

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

Torkel Ödegaard 11 лет назад
Родитель
Сommit
8a2541c220
79 измененных файлов с 1395 добавлено и 797 удалено
  1. 21 0
      .jsfmtrc
  2. 21 1
      CHANGELOG.md
  3. 2 2
      latest.json
  4. 1 1
      package.json
  5. 50 273
      src/app/components/kbn.js
  6. 2 0
      src/app/components/require.config.js
  7. 9 9
      src/app/components/timeSeries.js
  8. 1 0
      src/app/controllers/all.js
  9. 6 8
      src/app/controllers/dashboardCtrl.js
  10. 8 8
      src/app/controllers/dashboardNavCtrl.js
  11. 10 6
      src/app/controllers/grafanaCtrl.js
  12. 11 7
      src/app/controllers/graphiteImport.js
  13. 6 0
      src/app/controllers/row.js
  14. 3 2
      src/app/controllers/search.js
  15. 90 0
      src/app/controllers/sharePanelCtrl.js
  16. 1 2
      src/app/controllers/submenuCtrl.js
  17. 3 0
      src/app/controllers/templateEditorCtrl.js
  18. 2 1
      src/app/directives/dashEditLink.js
  19. 7 5
      src/app/directives/dashUpload.js
  20. 33 59
      src/app/directives/grafanaGraph.js
  21. 122 0
      src/app/directives/grafanaGraph.tooltip.js
  22. 11 29
      src/app/directives/grafanaPanel.js
  23. 130 0
      src/app/directives/panelMenu.js
  24. 23 0
      src/app/directives/tip.js
  25. 12 41
      src/app/panels/graph/axisEditor.html
  26. 1 1
      src/app/panels/graph/module.html
  27. 3 2
      src/app/panels/graph/module.js
  28. 17 19
      src/app/panels/graph/styleEditor.html
  29. 1 0
      src/app/panels/text/module.js
  30. 1 4
      src/app/panels/timepicker/module.html
  31. 1 1
      src/app/panels/timepicker/module.js
  32. 1 4
      src/app/partials/annotations_editor.html
  33. 10 2
      src/app/partials/dashboard.html
  34. 7 15
      src/app/partials/dasheditor.html
  35. 2 0
      src/app/partials/graphite/editor.html
  36. 8 4
      src/app/partials/import.html
  37. 2 1
      src/app/partials/playlist.html
  38. 2 6
      src/app/partials/roweditor.html
  39. 33 0
      src/app/partials/share-panel.html
  40. 0 7
      src/app/partials/submenu.html
  41. 6 12
      src/app/partials/templating_editor.html
  42. 3 4
      src/app/partials/unsaved-changes.html
  43. 23 6
      src/app/routes/dashboard-from-db.js
  44. 1 1
      src/app/routes/dashboard-from-file.js
  45. 1 1
      src/app/routes/dashboard-from-script.js
  46. 13 1
      src/app/services/alertSrv.js
  47. 1 0
      src/app/services/all.js
  48. 2 1
      src/app/services/annotationsSrv.js
  49. 4 4
      src/app/services/dashboard/dashboardKeyBindings.js
  50. 1 1
      src/app/services/dashboard/dashboardSrv.js
  51. 4 0
      src/app/services/elasticsearch/es-datasource.js
  52. 6 4
      src/app/services/influxdb/influxdbDatasource.js
  53. 34 39
      src/app/services/panelSrv.js
  54. 1 1
      src/app/services/timeSrv.js
  55. 31 0
      src/app/services/utilSrv.js
  56. 97 95
      src/config.sample.js
  57. 1 1
      src/css/less/bootswatch.dark.less
  58. 28 0
      src/css/less/forms.less
  59. 22 4
      src/css/less/grafana.less
  60. 17 1
      src/css/less/graph.less
  61. 0 11
      src/css/less/overrides.less
  62. 43 14
      src/css/less/panel.less
  63. 1 1
      src/css/less/submenu.less
  64. 4 2
      src/css/less/variables.dark.less
  65. 4 3
      src/css/less/variables.light.less
  66. BIN
      src/img/check_radio_sheet.png
  67. BIN
      src/img/checkbox.png
  68. BIN
      src/img/checkbox_white.png
  69. 16 0
      src/test/specs/dashboardSrv-specs.js
  70. 17 1
      src/test/specs/grafanaGraph-specs.js
  71. 56 0
      src/test/specs/graph-tooltip-specs.js
  72. 9 3
      src/test/specs/helpers.js
  73. 19 64
      src/test/specs/kbn-format-specs.js
  74. 71 0
      src/test/specs/sharePanelCtrl-specs.js
  75. 4 0
      src/test/test-main.js
  76. 2 1
      src/vendor/angular/angular-strap.js
  77. 1 1
      src/vendor/bootstrap/less/modals.less
  78. 176 0
      src/vendor/jquery/jquery.flot.crosshair.js
  79. 2 0
      src/vendor/jquery/jquery.flot.js

+ 21 - 0
.jsfmtrc

@@ -0,0 +1,21 @@
+{
+  "preset" : "default",
+
+  "lineBreak" : {
+    "before" : {
+      "VariableDeclarationWithoutInit" : 0,
+    },
+
+    "after": {
+      "AssignmentOperator": -1,
+      "ArgumentListArrayExpression": ">=1"
+    }
+  },
+
+  "whiteSpace" : {
+    "before" : {
+    },
+    "after" : {
+    }
+  }
+}

+ 21 - 1
CHANGELOG.md

@@ -1,4 +1,24 @@
-# 1.8.0 (unreleased)
+# 1.9.0 (unreleased)
+
+**UI Improvements*
+- [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
+
+- [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
+
+# 1.8.1 (unreleased)
+
+**Fixes**
+- [Issue #847](https://github.com/grafana/grafana/issues/847). Graph: Fix for series draw order not being the same after hiding/unhiding series
+- [Issue #851](https://github.com/grafana/grafana/issues/851). Annotations: Fix for annotations not reloaded when switching between 2 dashboards with annotations
+- [Issue #846](https://github.com/grafana/grafana/issues/846). Edit panes: Issue when open row or json editor when scrolled down the page, unable to scroll and you did not see editor
+- [Issue #840](https://github.com/grafana/grafana/issues/840). Import: Fixes to import from json file and import from graphite. Issues was lingering state from previous dashboard.
+- [Issue #859](https://github.com/grafana/grafana/issues/859). InfluxDB: Fix for bug when saving dashboard where title is the same as slugified url id
+- [Issue #852](https://github.com/grafana/grafana/issues/852). White theme: Fixes for hidden series legend text and disabled annotations color
+
+# 1.8.0 (2014-09-22)
+
+Read this [blog post](http://grafana.org/blog/2014/09/11/grafana-1-8-0-rc1-released.html) for an overview of all improvements.
 
 
 **Fixes**
 **Fixes**
 - [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource
 - [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource

+ 2 - 2
latest.json

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

+ 1 - 1
package.json

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

+ 50 - 273
src/app/components/kbn.js

@@ -7,6 +7,7 @@ function($, _, moment) {
   'use strict';
   'use strict';
 
 
   var kbn = {};
   var kbn = {};
+  kbn.valueFormats = {};
 
 
   kbn.round_interval = function(interval) {
   kbn.round_interval = function(interval) {
     switch (true) {
     switch (true) {
@@ -309,240 +310,27 @@ function($, _, moment) {
     ].join(';') + '"></div>';
     ].join(';') + '"></div>';
   };
   };
 
 
-  kbn.byteFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1024) {
-      steps++;
-      size /= 1024;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " B";
-      break;
-    case 1:
-      ext = " KiB";
-      break;
-    case 2:
-      ext = " MiB";
-      break;
-    case 3:
-      ext = " GiB";
-      break;
-    case 4:
-      ext = " TiB";
-      break;
-    case 5:
-      ext = " PiB";
-      break;
-    case 6:
-      ext = " EiB";
-      break;
-    case 7:
-      ext = " ZiB";
-      break;
-    case 8:
-      ext = " YiB";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
+  kbn.valueFormats.percent = function(size, decimals) {
+    return kbn.toFixed(size, decimals) + '%';
   };
   };
 
 
-  kbn.bitFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1024) {
-      steps++;
-      size /= 1024;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " b";
-      break;
-    case 1:
-      ext = " Kib";
-      break;
-    case 2:
-      ext = " Mib";
-      break;
-    case 3:
-      ext = " Gib";
-      break;
-    case 4:
-      ext = " Tib";
-      break;
-    case 5:
-      ext = " Pib";
-      break;
-    case 6:
-      ext = " Eib";
-      break;
-    case 7:
-      ext = " Zib";
-      break;
-    case 8:
-      ext = " Yib";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
-
-  kbn.bpsFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1000) {
-      steps++;
-      size /= 1000;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " bps";
-      break;
-    case 1:
-      ext = " Kbps";
-      break;
-    case 2:
-      ext = " Mbps";
-      break;
-    case 3:
-      ext = " Gbps";
-      break;
-    case 4:
-      ext = " Tbps";
-      break;
-    case 5:
-      ext = " Pbps";
-      break;
-    case 6:
-      ext = " Ebps";
-      break;
-    case 7:
-      ext = " Zbps";
-      break;
-    case 8:
-      ext = " Ybps";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
+  kbn.formatFuncCreator = function(factor, extArray) {
+    return function(size, decimals, scaledDecimals) {
+      var steps = 0;
 
 
-  kbn.shortFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1000) {
-      steps++;
-      size /= 1000;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = "";
-      break;
-    case 1:
-      ext = " K";
-      break;
-    case 2:
-      ext = " Mil";
-      break;
-    case 3:
-      ext = " Bil";
-      break;
-    case 4:
-      ext = " Tri";
-      break;
-    case 5:
-      ext = " Quadr";
-      break;
-    case 6:
-      ext = " Quint";
-      break;
-    case 7:
-      ext = " Sext";
-      break;
-    case 8:
-      ext = " Sept";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
+      while (Math.abs(size) >= factor) {
+        steps++;
+        size /= factor;
+      }
+      if (steps > 0) {
+        decimals = scaledDecimals + (3 * steps);
+      }
 
 
-  kbn.getFormatFunction = function(formatName, decimals) {
-    switch(formatName) {
-    case 'short':
-      return function(val) {
-        return kbn.shortFormat(val, decimals);
-      };
-    case 'bytes':
-      return function(val) {
-        return kbn.byteFormat(val, decimals);
-      };
-    case 'bits':
-      return function(val) {
-        return kbn.bitFormat(val, decimals);
-      };
-    case 'bps':
-      return function(val) {
-        return kbn.bpsFormat(val, decimals);
-      };
-    case 's':
-      return function(val) {
-        return kbn.sFormat(val, decimals);
-      };
-    case 'ms':
-      return function(val) {
-        return kbn.msFormat(val, decimals);
-      };
-    case 'µs':
-      return function(val) {
-        return kbn.microsFormat(val, decimals);
-      };
-    case 'ns':
-      return function(val) {
-        return kbn.nanosFormat(val, decimals);
-      };
-    case 'percent':
-      return function(val, axis) {
-        return kbn.noneFormat(val, axis ? axis.tickDecimals : null) + ' %';
-      };
-    default:
-      return function(val, axis) {
-        return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
-      };
-    }
+      return kbn.toFixed(size, decimals) + extArray[steps];
+    };
   };
   };
 
 
-  kbn.noneFormat = function(value, decimals) {
+  kbn.toFixed = function(value, decimals) {
     var factor = decimals ? Math.pow(10, decimals) : 1;
     var factor = decimals ? Math.pow(10, decimals) : 1;
     var formatted = String(Math.round(value * factor) / factor);
     var formatted = String(Math.round(value * factor) / factor);
 
 
@@ -553,7 +341,6 @@ function($, _, moment) {
 
 
     // If tickDecimals was specified, ensure that we have exactly that
     // If tickDecimals was specified, ensure that we have exactly that
     // much precision; otherwise default to the value's own precision.
     // much precision; otherwise default to the value's own precision.
-
     if (decimals != null) {
     if (decimals != null) {
       var decimalPos = formatted.indexOf(".");
       var decimalPos = formatted.indexOf(".");
       var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
       var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
@@ -565,97 +352,87 @@ function($, _, moment) {
     return formatted;
     return formatted;
   };
   };
 
 
-  kbn.msFormat = function(size, decimals) {
-    // Less than 1 milli, downscale to micro
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.microsFormat(size * 1000, decimals);
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(decimals) + " ms";
+  kbn.valueFormats.bits = kbn.formatFuncCreator(1024, [' b', ' Kib', ' Mib', ' Gib', ' Tib', ' Pib', ' Eib', ' Zib', ' Yib']);
+  kbn.valueFormats.bytes = kbn.formatFuncCreator(1024, [' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']);
+  kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']);
+  kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']);
+  kbn.valueFormats.none = kbn.toFixed;
+
+  kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " ms";
     }
     }
     // Less than 1 min
     // Less than 1 min
     else if (Math.abs(size) < 60000) {
     else if (Math.abs(size) < 60000) {
-      return (size / 1000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " s";
     }
     }
     // Less than 1 hour, devide in minutes
     // Less than 1 hour, devide in minutes
     else if (Math.abs(size) < 3600000) {
     else if (Math.abs(size) < 3600000) {
-      return (size / 60000).toFixed(decimals) + " min";
+      return kbn.toFixed(size / 60000, scaledDecimals + 5) + " min";
     }
     }
     // Less than one day, devide in hours
     // Less than one day, devide in hours
     else if (Math.abs(size) < 86400000) {
     else if (Math.abs(size) < 86400000) {
-      return (size / 3600000).toFixed(decimals) + " hour";
+      return kbn.toFixed(size / 3600000, scaledDecimals + 7) + " hour";
     }
     }
     // Less than one year, devide in days
     // Less than one year, devide in days
     else if (Math.abs(size) < 31536000000) {
     else if (Math.abs(size) < 31536000000) {
-      return (size / 86400000).toFixed(decimals) + " day";
+      return kbn.toFixed(size / 86400000, scaledDecimals + 8) + " day";
     }
     }
 
 
-    return (size / 31536000000).toFixed(decimals) + " year";
+    return kbn.toFixed(size / 31536000000, scaledDecimals + 10) + " year";
   };
   };
 
 
-  kbn.sFormat = function(size, decimals) {
-    // Less than 1 sec, downscale to milli
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.msFormat(size * 1000, decimals);
-    }
-    // Less than 10 min, use seconds
-    else if (Math.abs(size) < 600) {
-      return size.toFixed(decimals) + " s";
+  kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 600) {
+      return kbn.toFixed(size, decimals) + " s";
     }
     }
     // Less than 1 hour, devide in minutes
     // Less than 1 hour, devide in minutes
     else if (Math.abs(size) < 3600) {
     else if (Math.abs(size) < 3600) {
-      return (size / 60).toFixed(decimals) + " min";
+      return kbn.toFixed(size / 60, scaledDecimals + 1) + " min";
     }
     }
     // Less than one day, devide in hours
     // Less than one day, devide in hours
     else if (Math.abs(size) < 86400) {
     else if (Math.abs(size) < 86400) {
-      return (size / 3600).toFixed(decimals) + " hour";
+      return kbn.toFixed(size / 3600, scaledDecimals + 4) + " hour";
     }
     }
     // Less than one week, devide in days
     // Less than one week, devide in days
     else if (Math.abs(size) < 604800) {
     else if (Math.abs(size) < 604800) {
-      return (size / 86400).toFixed(decimals) + " day";
+      return kbn.toFixed(size / 86400, scaledDecimals + 5) + " day";
     }
     }
     // Less than one year, devide in week
     // Less than one year, devide in week
     else if (Math.abs(size) < 31536000) {
     else if (Math.abs(size) < 31536000) {
-      return (size / 604800).toFixed(decimals) + " week";
+      return kbn.toFixed(size / 604800, scaledDecimals + 6) + " week";
     }
     }
 
 
-    return (size / 3.15569e7).toFixed(decimals) + " year";
+    return kbn.toFixed(size / 3.15569e7, scaledDecimals + 7) + " year";
   };
   };
 
 
-  kbn.microsFormat = function(size, decimals) {
-    // Less than 1 micro, downscale to nano
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.nanosFormat(size * 1000, decimals);
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(decimals) + " µs";
+  kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " µs";
     }
     }
     else if (Math.abs(size) < 1000000) {
     else if (Math.abs(size) < 1000000) {
-      return (size / 1000).toFixed(decimals) + " ms";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " ms";
     }
     }
     else {
     else {
-      return (size / 1000000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " s";
     }
     }
   };
   };
 
 
-  kbn.nanosFormat = function(size, decimals) {
-    if (Math.abs(size) < 1) {
-      return size.toFixed(decimals) + " ns";
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(0) + " ns";
+  kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " ns";
     }
     }
     else if (Math.abs(size) < 1000000) {
     else if (Math.abs(size) < 1000000) {
-      return (size / 1000).toFixed(decimals) + " µs";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " µs";
     }
     }
     else if (Math.abs(size) < 1000000000) {
     else if (Math.abs(size) < 1000000000) {
-      return (size / 1000000).toFixed(decimals) + " ms";
+      return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " ms";
     }
     }
     else if (Math.abs(size) < 60000000000){
     else if (Math.abs(size) < 60000000000){
-      return (size / 1000000000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000000000, scaledDecimals + 9) + " s";
     }
     }
     else {
     else {
-      return (size / 60000000000).toFixed(decimals) + " m";
+      return kbn.toFixed(size / 60000000000, scaledDecimals + 12) + " m";
     }
     }
   };
   };
 
 

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

@@ -41,6 +41,7 @@ require.config({
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
+    'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
 
 
@@ -84,6 +85,7 @@ require.config({
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
+    'jquery.flot.crosshair':['jquery', 'jquery.flot'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],
     'angular-dragdrop':     ['jquery','jquery-ui','angular'],
     'angular-dragdrop':     ['jquery','jquery-ui','angular'],
     'angular-loader':       ['angular'],
     'angular-loader':       ['angular'],

+ 9 - 9
src/app/components/timeSeries.js

@@ -54,7 +54,7 @@ function (_, kbn) {
     }
     }
   };
   };
 
 
-  TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
+  TimeSeries.prototype.getFlotPairs = function (fillStyle) {
     var result = [];
     var result = [];
 
 
     this.color = this.info.color;
     this.color = this.info.color;
@@ -100,21 +100,21 @@ function (_, kbn) {
     }
     }
 
 
     if (result.length) {
     if (result.length) {
-
       this.info.avg = (this.info.total / result.length);
       this.info.avg = (this.info.total / result.length);
       this.info.current = result[result.length-1][1];
       this.info.current = result[result.length-1][1];
-
-      var formater = kbn.getFormatFunction(yFormats[this.yaxis - 1], 2);
-      this.info.avg = this.info.avg != null ? formater(this.info.avg) : null;
-      this.info.current = this.info.current != null ? formater(this.info.current) : null;
-      this.info.min = this.info.min != null ? formater(this.info.min) : null;
-      this.info.max = this.info.max != null ? formater(this.info.max) : null;
-      this.info.total = this.info.total != null ? formater(this.info.total) : null;
     }
     }
 
 
     return result;
     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;
+  };
+
   return TimeSeries;
   return TimeSeries;
 
 
 });
 });

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

@@ -15,5 +15,6 @@ define([
   './opentsdbTargetCtrl',
   './opentsdbTargetCtrl',
   './annotationsEditorCtrl',
   './annotationsEditorCtrl',
   './templateEditorCtrl',
   './templateEditorCtrl',
+  './sharePanelCtrl',
   './jsonEditorCtrl',
   './jsonEditorCtrl',
 ], function () {});
 ], function () {});

+ 6 - 8
src/app/controllers/dashboardCtrl.js

@@ -19,19 +19,18 @@ function (angular, $, config, _) {
       dashboardSrv,
       dashboardSrv,
       dashboardViewStateSrv,
       dashboardViewStateSrv,
       panelMoveSrv,
       panelMoveSrv,
-      timer,
       $timeout) {
       $timeout) {
 
 
     $scope.editor = { index: 0 };
     $scope.editor = { index: 0 };
     $scope.panelNames = config.panels;
     $scope.panelNames = config.panels;
     var resizeEventTimeout;
     var resizeEventTimeout;
 
 
-    $scope.init = function() {
+    this.init = function(dashboardData) {
       $scope.availablePanels = config.panels;
       $scope.availablePanels = config.panels;
-      $scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
-      $scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
       $scope.reset_row();
       $scope.reset_row();
       $scope.registerWindowResizeEvent();
       $scope.registerWindowResizeEvent();
+      $scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
+      $scope.setupDashboard(dashboardData);
     };
     };
 
 
     $scope.registerWindowResizeEvent = function() {
     $scope.registerWindowResizeEvent = function() {
@@ -41,7 +40,7 @@ function (angular, $, config, _) {
       });
       });
     };
     };
 
 
-    $scope.setupDashboard = function(event, dashboardData) {
+    $scope.setupDashboard = function(dashboardData) {
       $rootScope.performance.dashboardLoadStart = new Date().getTime();
       $rootScope.performance.dashboardLoadStart = new Date().getTime();
       $rootScope.performance.panelsInitialized = 0;
       $rootScope.performance.panelsInitialized = 0;
       $rootScope.performance.panelsRendered = 0;
       $rootScope.performance.panelsRendered = 0;
@@ -59,7 +58,7 @@ function (angular, $, config, _) {
 
 
       $scope.setWindowTitleAndTheme();
       $scope.setWindowTitleAndTheme();
 
 
-      $scope.emitAppEvent("dashboard-loaded", $scope.dashboard);
+      $scope.appEvent("dashboard-loaded", $scope.dashboard);
     };
     };
 
 
     $scope.setWindowTitleAndTheme = function() {
     $scope.setWindowTitleAndTheme = function() {
@@ -114,7 +113,7 @@ function (angular, $, config, _) {
       var editScope = $rootScope.$new();
       var editScope = $rootScope.$new();
       editScope.object = options.object;
       editScope.object = options.object;
       editScope.updateHandler = options.updateHandler;
       editScope.updateHandler = options.updateHandler;
-      $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
+      $scope.appEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
     };
     };
 
 
     $scope.checkFeatureToggles = function() {
     $scope.checkFeatureToggles = function() {
@@ -129,6 +128,5 @@ function (angular, $, config, _) {
       return $scope.editorTabs;
       return $scope.editorTabs;
     };
     };
 
 
-    $scope.init();
   });
   });
 });
 });

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

@@ -69,7 +69,7 @@ function (angular, _, moment, config, store) {
     };
     };
 
 
     $scope.openSearch = function() {
     $scope.openSearch = function() {
-      $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
+      $scope.appEvent('show-dash-editor', { src: 'app/partials/search.html' });
     };
     };
 
 
     $scope.saveDashboard = function() {
     $scope.saveDashboard = function() {
@@ -78,7 +78,7 @@ function (angular, _, moment, config, store) {
       var clone = angular.copy($scope.dashboard);
       var clone = angular.copy($scope.dashboard);
       $scope.db.saveDashboard(clone)
       $scope.db.saveDashboard(clone)
         .then(function(result) {
         .then(function(result) {
-          alertSrv.set('Dashboard Saved', 'Saved as "' + result.title + '"','success', 3000);
+          $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + result.title]);
 
 
           if (result.url !== $location.path()) {
           if (result.url !== $location.path()) {
             $location.search({});
             $location.search({});
@@ -88,12 +88,12 @@ function (angular, _, moment, config, store) {
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
 
 
         }, function(err) {
         }, function(err) {
-          alertSrv.set('Save failed', err, 'error', 5000);
+          $scope.appEvent('alert-error', ['Save failed', err]);
         });
         });
     };
     };
 
 
     $scope.deleteDashboard = function(evt, options) {
     $scope.deleteDashboard = function(evt, options) {
-      if (!confirm('Are you sure you want to delete dashboard?')) {
+      if (!confirm('Do you want to delete dashboard ' + options.title + ' ?')) {
         return;
         return;
       }
       }
 
 
@@ -101,9 +101,9 @@ function (angular, _, moment, config, store) {
 
 
       var id = options.id;
       var id = options.id;
       $scope.db.deleteDashboard(id).then(function(id) {
       $scope.db.deleteDashboard(id).then(function(id) {
-        alertSrv.set('Dashboard Deleted', id + ' has been deleted', 'success', 5000);
-      }, function() {
-        alertSrv.set('Dashboard Not Deleted', 'An error occurred deleting the dashboard', 'error', 5000);
+        $scope.appEvent('alert-success', ['Dashboard Deleted', id + ' has been deleted']);
+      }, function(err) {
+        $scope.appEvent('alert-error', ['Deleted failed', err]);
       });
       });
     };
     };
 
 
@@ -138,7 +138,7 @@ function (angular, _, moment, config, store) {
     };
     };
 
 
     $scope.editJson = function() {
     $scope.editJson = function() {
-      $scope.emitAppEvent('show-json-editor', { object: $scope.dashboard });
+      $scope.appEvent('show-json-editor', { object: $scope.dashboard });
     };
     };
 
 
     $scope.openSaveDropdown = function() {
     $scope.openSaveDropdown = function() {

+ 10 - 6
src/app/controllers/grafanaCtrl.js

@@ -10,19 +10,19 @@ function (angular, config, _, $, store) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
+  module.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, grafanaVersion, $rootScope, $controller) {
 
 
     $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
     $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
-    $scope.consoleEnabled = store.getBool('grafanaConsole');
-
+    $scope._ = _;
     $rootScope.profilingEnabled = store.getBool('profilingEnabled');
     $rootScope.profilingEnabled = store.getBool('profilingEnabled');
     $rootScope.performance = { loadStart: new Date().getTime() };
     $rootScope.performance = { loadStart: new Date().getTime() };
 
 
     $scope.init = function() {
     $scope.init = function() {
-      $scope._ = _;
-
       if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
       if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
 
 
+      alertSrv.init();
+      utilSrv.init();
+
       $scope.dashAlerts = alertSrv;
       $scope.dashAlerts = alertSrv;
       $scope.grafana = { style: 'dark' };
       $scope.grafana = { style: 'dark' };
     };
     };
@@ -32,12 +32,16 @@ function (angular, config, _, $, store) {
       store.set('grafanaConsole', $scope.consoleEnabled);
       store.set('grafanaConsole', $scope.consoleEnabled);
     };
     };
 
 
+    $scope.initDashboard = function(dashboardData, viewScope) {
+      $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData);
+    };
+
     $rootScope.onAppEvent = function(name, callback) {
     $rootScope.onAppEvent = function(name, callback) {
       var unbind = $rootScope.$on(name, callback);
       var unbind = $rootScope.$on(name, callback);
       this.$on('$destroy', unbind);
       this.$on('$destroy', unbind);
     };
     };
 
 
-    $rootScope.emitAppEvent = function(name, payload) {
+    $rootScope.appEvent = function(name, payload) {
       $rootScope.$emit(name, payload);
       $rootScope.$emit(name, payload);
     };
     };
 
 

+ 11 - 7
src/app/controllers/graphiteImport.js

@@ -1,14 +1,15 @@
 define([
 define([
   'angular',
   'angular',
   'app',
   'app',
-  'lodash'
+  'lodash',
+  'kbn'
 ],
 ],
-function (angular, app, _) {
+function (angular, app, _, kbn) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv) {
+  module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv, $location) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.datasources = datasourceSrv.getMetricSources();
       $scope.datasources = datasourceSrv.getMetricSources();
@@ -72,18 +73,19 @@ function (angular, app, _) {
       newDashboard.title = state.name;
       newDashboard.title = state.name;
       newDashboard.rows.push(currentRow);
       newDashboard.rows.push(currentRow);
 
 
-      _.each(state.graphs, function(graph) {
+      _.each(state.graphs, function(graph, index) {
         if (currentRow.panels.length === graphsPerRow) {
         if (currentRow.panels.length === graphsPerRow) {
           currentRow = angular.copy(rowTemplate);
           currentRow = angular.copy(rowTemplate);
           newDashboard.rows.push(currentRow);
           newDashboard.rows.push(currentRow);
         }
         }
 
 
         panel = {
         panel = {
-          type: 'graphite',
+          type: 'graph',
           span: 12 / graphsPerRow,
           span: 12 / graphsPerRow,
           title: graph[1].title,
           title: graph[1].title,
           targets: [],
           targets: [],
-          datasource: datasource
+          datasource: datasource,
+          id: index + 1
         };
         };
 
 
         _.each(graph[1].target, function(target) {
         _.each(graph[1].target, function(target) {
@@ -95,7 +97,9 @@ function (angular, app, _) {
         currentRow.panels.push(panel);
         currentRow.panels.push(panel);
       });
       });
 
 
-      $scope.emitAppEvent('setup-dashboard', newDashboard);
+      window.grafanaImportDashboard = newDashboard;
+      $location.path('/dashboard/import/' + kbn.slugifyForUrl(newDashboard.title));
+
       $scope.dismiss();
       $scope.dismiss();
     }
     }
 
 

+ 6 - 0
src/app/controllers/row.js

@@ -13,6 +13,7 @@ function (angular, app, _) {
       title: "Row",
       title: "Row",
       height: "150px",
       height: "150px",
       collapse: false,
       collapse: false,
+      editable: true,
       panels: [],
       panels: [],
     };
     };
 
 
@@ -22,6 +23,11 @@ function (angular, app, _) {
       $scope.reset_panel();
       $scope.reset_panel();
     };
     };
 
 
+    $scope.togglePanelMenu = function(posX) {
+      $scope.showPanelMenu = !$scope.showPanelMenu;
+      $scope.panelMenuPos = posX;
+    };
+
     $scope.toggle_row = function(row) {
     $scope.toggle_row = function(row) {
       row.collapse = row.collapse ? false : true;
       row.collapse = row.collapse ? false : true;
       if (!row.collapse) {
       if (!row.collapse) {

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

@@ -29,7 +29,7 @@ function (angular, _, config, $) {
 
 
     $scope.keyDown = function (evt) {
     $scope.keyDown = function (evt) {
       if (evt.keyCode === 27) {
       if (evt.keyCode === 27) {
-        $scope.emitAppEvent('hide-dash-editor');
+        $scope.appEvent('hide-dash-editor');
       }
       }
       if (evt.keyCode === 40) {
       if (evt.keyCode === 40) {
         $scope.moveSelection(1);
         $scope.moveSelection(1);
@@ -62,6 +62,7 @@ function (angular, _, config, $) {
     };
     };
 
 
     $scope.goToDashboard = function(id) {
     $scope.goToDashboard = function(id) {
+      $location.search({});
       $location.path("/dashboard/db/" + id);
       $location.path("/dashboard/db/" + id);
     };
     };
 
 
@@ -121,7 +122,7 @@ function (angular, _, config, $) {
 
 
     $scope.deleteDashboard = function(dash, evt) {
     $scope.deleteDashboard = function(dash, evt) {
       evt.stopPropagation();
       evt.stopPropagation();
-      $scope.emitAppEvent('delete-dashboard', { id: dash.id });
+      $scope.appEvent('delete-dashboard', { id: dash.id, title: dash.title });
       $scope.results.dashboards = _.without($scope.results.dashboards, dash);
       $scope.results.dashboards = _.without($scope.results.dashboards, dash);
     };
     };
 
 

+ 90 - 0
src/app/controllers/sharePanelCtrl.js

@@ -0,0 +1,90 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('SharePanelCtrl', function($scope, $location, $timeout, timeSrv, $element, templateSrv) {
+
+    $scope.init = function() {
+      $scope.editor = { index: 0 };
+      $scope.forCurrent = true;
+      $scope.toPanel = true;
+      $scope.includeTemplateVars = true;
+
+      $scope.buildUrl();
+    };
+
+    $scope.buildUrl = function() {
+      var baseUrl = $location.absUrl();
+      var queryStart = baseUrl.indexOf('?');
+
+      if (queryStart !== -1) {
+        baseUrl = baseUrl.substring(0, queryStart);
+      }
+
+      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();
+      }
+
+      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;
+        });
+      }
+      else {
+        _.each(templateSrv.variables, function(variable) {
+          delete params['var-' + variable.name];
+        });
+      }
+
+      if (!$scope.forCurrent) {
+        delete params.from;
+        delete params.to;
+      }
+
+      if ($scope.toPanel) {
+        params.panelId = panelId;
+        params.fullscreen = true;
+      } else {
+        delete params.panelId;
+        delete params.fullscreen;
+      }
+
+      var paramsArray = [];
+      _.each(params, function(value, key) {
+        var str = key;
+        if (value !== true) {
+          str += '=' + encodeURIComponent(value);
+        }
+        paramsArray.push(str);
+      });
+
+      $scope.shareUrl = baseUrl + "?" + paramsArray.join('&') ;
+
+      $timeout(function() {
+        var input = $element.find('[data-share-panel-url]');
+        input.focus();
+        input.select();
+      }, 10);
+
+    };
+
+    $scope.init();
+
+  });
+
+});

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

@@ -1,9 +1,8 @@
 define([
 define([
   'angular',
   'angular',
-  'app',
   'lodash'
   'lodash'
 ],
 ],
-function (angular, app, _) {
+function (angular, _) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');

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

@@ -72,6 +72,9 @@ function (angular, _) {
       if ($scope.current.type === 'interval') {
       if ($scope.current.type === 'interval') {
         $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
         $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
       }
       }
+      if ($scope.current.type === 'query') {
+        $scope.current.query = '';
+      }
     };
     };
 
 
     $scope.removeVariable = function(variable) {
     $scope.removeVariable = function(variable) {

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

@@ -16,7 +16,7 @@ function (angular, $) {
           elem.bind('click',function() {
           elem.bind('click',function() {
             $timeout(function() {
             $timeout(function() {
               var editorScope = attrs.editorScope === 'isolated' ? null : scope;
               var editorScope = attrs.editorScope === 'isolated' ? null : scope;
-              scope.emitAppEvent('show-dash-editor', { src: partial, scope: editorScope });
+              scope.appEvent('show-dash-editor', { src: partial, scope: editorScope });
             });
             });
           });
           });
         }
         }
@@ -34,6 +34,7 @@ function (angular, $) {
 
 
           function hideScrollbars(value) {
           function hideScrollbars(value) {
             if (value) {
             if (value) {
+              window.scrollTo(0,0);
               document.documentElement.style.overflow = 'hidden';  // firefox, chrome
               document.documentElement.style.overflow = 'hidden';  // firefox, chrome
               document.body.scroll = "no"; // ie only
               document.body.scroll = "no"; // ie only
             } else {
             } else {

+ 7 - 5
src/app/directives/dashUpload.js

@@ -1,12 +1,13 @@
 define([
 define([
-  'angular'
+  'angular',
+  'kbn'
 ],
 ],
-function (angular) {
+function (angular, kbn) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
 
 
-  module.directive('dashUpload', function(timer, alertSrv) {
+  module.directive('dashUpload', function(timer, alertSrv, $location) {
     return {
     return {
       restrict: 'A',
       restrict: 'A',
       link: function(scope) {
       link: function(scope) {
@@ -14,9 +15,10 @@ function (angular) {
           var files = evt.target.files; // FileList object
           var files = evt.target.files; // FileList object
           var readerOnload = function() {
           var readerOnload = function() {
             return function(e) {
             return function(e) {
-              var dashboard = JSON.parse(e.target.result);
               scope.$apply(function() {
               scope.$apply(function() {
-                scope.emitAppEvent('setup-dashboard', dashboard);
+                window.grafanaImportDashboard = JSON.parse(e.target.result);
+                var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title);
+                $location.path('/dashboard/import/' + title);
               });
               });
             };
             };
           };
           };

+ 33 - 59
src/app/directives/grafanaGraph.js

@@ -3,9 +3,10 @@ define([
   'jquery',
   'jquery',
   'kbn',
   'kbn',
   'moment',
   'moment',
-  'lodash'
+  'lodash',
+  './grafanaGraph.tooltip'
 ],
 ],
-function (angular, $, kbn, moment, _) {
+function (angular, $, kbn, moment, _, graphTooltip) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
@@ -15,23 +16,15 @@ function (angular, $, kbn, moment, _) {
       restrict: 'A',
       restrict: 'A',
       template: '<div> </div>',
       template: '<div> </div>',
       link: function(scope, elem) {
       link: function(scope, elem) {
-        var data, annotations;
-        var hiddenData = {};
         var dashboard = scope.dashboard;
         var dashboard = scope.dashboard;
+        var data, annotations;
         var legendSideLastValue = null;
         var legendSideLastValue = null;
 
 
         scope.$on('refresh',function() {
         scope.$on('refresh',function() {
           scope.get_data();
           scope.get_data();
         });
         });
 
 
-        scope.$on('toggleLegend', function(e, series) {
-          _.each(series, function(serie) {
-            if (hiddenData[serie.alias]) {
-              data.push(hiddenData[serie.alias]);
-              delete hiddenData[serie.alias];
-            }
-          });
-
+        scope.$on('toggleLegend', function() {
           render_panel();
           render_panel();
         });
         });
 
 
@@ -88,6 +81,18 @@ function (angular, $, kbn, moment, _) {
           }
           }
         }
         }
 
 
+        function updateLegendValues(plot) {
+          var yaxis = plot.getYAxes();
+
+          for (var i = 0; i < data.length; i++) {
+            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);
+          }
+
+        }
+
         // Function for rendering panel
         // Function for rendering panel
         function render_panel() {
         function render_panel() {
           if (shouldAbortRender()) {
           if (shouldAbortRender()) {
@@ -95,21 +100,11 @@ function (angular, $, kbn, moment, _) {
           }
           }
 
 
           var panel = scope.panel;
           var panel = scope.panel;
-
-          _.each(_.keys(scope.hiddenSeries), function(seriesAlias) {
-            var dataSeries = _.find(data, function(series) {
-              return series.info.alias === seriesAlias;
-            });
-            if (dataSeries) {
-              hiddenData[dataSeries.info.alias] = dataSeries;
-              data = _.without(data, dataSeries);
-            }
-          });
-
           var stack = panel.stack ? true : null;
           var stack = panel.stack ? true : null;
 
 
           // Populate element
           // Populate element
           var options = {
           var options = {
+            hooks: { draw: [updateLegendValues] },
             legend: { show: false },
             legend: { show: false },
             series: {
             series: {
               stackpercent: panel.stack ? panel.percentage : false,
               stackpercent: panel.stack ? panel.percentage : false,
@@ -132,7 +127,8 @@ function (angular, $, kbn, moment, _) {
                 show: panel.points,
                 show: panel.points,
                 fill: 1,
                 fill: 1,
                 fillColor: false,
                 fillColor: false,
-                radius: panel.pointradius
+                radius: panel.points ? panel.pointradius : 2
+                // little points when highlight points
               },
               },
               shadowSize: 1
               shadowSize: 1
             },
             },
@@ -149,6 +145,9 @@ function (angular, $, kbn, moment, _) {
             selection: {
             selection: {
               mode: "x",
               mode: "x",
               color: '#666'
               color: '#666'
+            },
+            crosshair: {
+              mode: panel.tooltip.shared ? "x" : null
             }
             }
           };
           };
 
 
@@ -156,6 +155,11 @@ function (angular, $, kbn, moment, _) {
             var series = data[i];
             var series = data[i];
             series.applySeriesOverrides(panel.seriesOverrides);
             series.applySeriesOverrides(panel.seriesOverrides);
             series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
             series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
+            // if hidden remove points and disable stack
+            if (scope.hiddenSeries[series.info.alias]) {
+              series.data = [];
+              series.stack = false;
+            }
           }
           }
 
 
           if (data.length && data[0].info.timeStep) {
           if (data.length && data[0].info.timeStep) {
@@ -313,7 +317,9 @@ function (angular, $, kbn, moment, _) {
         }
         }
 
 
         function configureAxisMode(axis, format) {
         function configureAxisMode(axis, format) {
-          axis.tickFormatter = kbn.getFormatFunction(format, 1);
+          axis.tickFormatter = function(val, axis) {
+            return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+          };
         }
         }
 
 
         function time_format(interval, ticks, min, max) {
         function time_format(interval, ticks, min, max) {
@@ -338,40 +344,6 @@ function (angular, $, kbn, moment, _) {
           return "%H:%M";
           return "%H:%M";
         }
         }
 
 
-        var $tooltip = $('<div id="tooltip">');
-
-        elem.bind("plothover", function (event, pos, item) {
-          var group, value, timestamp, seriesInfo, format;
-
-          if (item) {
-            seriesInfo = item.series.info;
-            format = scope.panel.y_formats[seriesInfo.yaxis - 1];
-
-            if (seriesInfo.alias) {
-              group = '<small style="font-size:0.9em;">' +
-                '<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
-                seriesInfo.alias +
-              '</small><br>';
-            } else {
-              group = kbn.query_color_dot(item.series.color, 15) + ' ';
-            }
-
-            if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
-              value = item.datapoint[1] - item.datapoint[2];
-            }
-            else {
-              value = item.datapoint[1];
-            }
-
-            value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis);
-            timestamp = dashboard.formatDate(item.datapoint[0]);
-
-            $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
-          } else {
-            $tooltip.detach();
-          }
-        });
-
         function render_panel_as_graphite_png(url) {
         function render_panel_as_graphite_png(url) {
           url += '&width=' + elem.width();
           url += '&width=' + elem.width();
           url += '&height=' + elem.css('height').replace('px', '');
           url += '&height=' + elem.css('height').replace('px', '');
@@ -422,6 +394,8 @@ function (angular, $, kbn, moment, _) {
           elem.html('<img src="' + url + '"></img>');
           elem.html('<img src="' + url + '"></img>');
         }
         }
 
 
+        graphTooltip.register(elem, dashboard, scope);
+
         elem.bind("plotselected", function (event, ranges) {
         elem.bind("plotselected", function (event, ranges) {
           scope.$apply(function() {
           scope.$apply(function() {
             timeSrv.setTime({
             timeSrv.setTime({

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

@@ -0,0 +1,122 @@
+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) {
+        var plot = elem.data().plot;
+        $tooltip.detach();
+        plot.clearCrosshair();
+        plot.unhighlight();
+      }
+    });
+
+    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;
+    }
+
+    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 (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) {
+            console.log('WARNING: tootltip shared can not be shown becouse of series points do not align, different point counts');
+            $tooltip.detach();
+            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 && scope.panel.tooltip.value_type === 'individual') {
+            value = series.data[hoverIndex][1];
+          } else {
+            last_value += series.data[hoverIndex][1];
+            value = last_value;
+          }
+
+          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);
+        }
+
+        $tooltip.html('<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ timestamp + '</div> ' + seriesHtml + '</div>')
+          .place_tt(pos.pageX + 20, pos.pageY);
+        return;
+      }
+      if (item) {
+        seriesInfo = item.series.info;
+        format = scope.panel.y_formats[seriesInfo.yaxis - 1];
+
+        if (seriesInfo.alias) {
+          group = '<small style="font-size:0.9em;">' +
+            '<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
+            seriesInfo.alias +
+            '</small><br>';
+        } else {
+          group = kbn.query_color_dot(item.series.color, 15) + ' ';
+        }
+
+        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]);
+
+        $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
+      } else {
+        $tooltip.detach();
+      }
+    });
+
+  }
+
+  return {
+    register: registerTooltipFeatures
+  };
+});

+ 11 - 29
src/app/directives/grafanaPanel.js

@@ -1,7 +1,7 @@
 define([
 define([
   'angular',
   'angular',
   'jquery',
   'jquery',
-  'lodash',
+  './panelMenu',
 ],
 ],
 function (angular, $) {
 function (angular, $) {
   'use strict';
   'use strict';
@@ -15,37 +15,19 @@ function (angular, $) {
 
 
       var panelHeader =
       var panelHeader =
       '<div class="panel-header">'+
       '<div class="panel-header">'+
-       '<div class="row-fluid panel-extra">' +
-          '<div class="panel-extra-container">' +
-            '<span class="alert-error panel-error small pointer"' +
-                  'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' +
-              '<span data-placement="right" bs-tooltip="panelMeta.error">' +
-              '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
-              '</span>' +
+          '<span class="alert-error panel-error small pointer"' +
+                'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' +
+            '<span data-placement="right" bs-tooltip="panelMeta.error">' +
+            '<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
             '</span>' +
             '</span>' +
+          '</span>' +
 
 
-            '<span class="panel-loading" ng-show="panelMeta.loading">' +
-              '<i class="icon-spinner icon-spin icon-large"></i>' +
-            '</span>' +
-
-            '<span class="dropdown">' +
-              '<span class="panel-text panel-title pointer" gf-dropdown="panelMeta.menu" tabindex="1" ' +
-              'data-drag=true data-jqyoui-options="kbnJqUiDraggableOptions"'+
-              ' jqyoui-draggable="'+
-              '{'+
-                'animate:false,'+
-                'mutate:false,'+
-                'index:{{$index}},'+
-                'onStart:\'panelMoveStart\','+
-                'onStop:\'panelMoveStop\''+
-                '}"  ng-model="panel" ' +
-                '>' +
-                '{{panel.title | interpolateTemplateVars}}' +
-              '</span>' +
-            '</span>'+
+          '<span class="panel-loading" ng-show="panelMeta.loading">' +
+            '<i class="icon-spinner icon-spin icon-large"></i>' +
+          '</span>' +
 
 
-          '</div>'+
-        '</div>\n'+
+          '<div class="panel-title-container" panel-menu></div>' +
+        '</div>'+
       '</div>';
       '</div>';
 
 
       return {
       return {

+ 130 - 0
src/app/directives/panelMenu.js

@@ -0,0 +1,130 @@
+define([
+  'angular',
+  'jquery',
+  'lodash',
+],
+function (angular, $, _) {
+  'use strict';
+
+  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" ';
+
+      function createMenuTemplate($scope) {
+        var template = '<div class="panel-menu small">';
+        template += '<div class="panel-menu-inner">';
+        template += '<div class="panel-menu-row">';
+        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">';
+
+        _.each($scope.panelMeta.menu, function(item) {
+          template += '<a class="panel-menu-link" ';
+          if (item.click) { template += ' ng-click="' + item.click + '"'; }
+          if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
+          template += '>';
+          template += item.text + '</a>';
+        });
+
+        template += '</div>';
+        template += '</div>';
+        template += '</div>';
+        return template;
+      }
+
+      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 menuScope = null;
+          var timeout = null;
+          var $menu = null;
+
+          elem.append($link);
+
+          function dismiss(time) {
+            clearTimeout(timeout);
+            timeout = null;
+
+            if (time) {
+              timeout = setTimeout(dismiss, time);
+              return;
+            }
+
+            // if hovering or draging pospone close
+            if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) {
+              dismiss(2500);
+              return;
+            }
+
+            if (menuScope) {
+              $menu.unbind();
+              $menu.remove();
+              menuScope.$destroy();
+              menuScope = null;
+              $menu = null;
+              $panelContainer.removeClass('panel-highlight');
+            }
+          }
+
+          var showMenu = function() {
+            if ($menu) {
+              dismiss();
+              return;
+            }
+
+            var windowWidth = $(window).width();
+            var panelLeftPos = $(elem).offset().left;
+            var panelWidth = $(elem).width();
+            var menuLeftPos = (panelWidth / 2) - (menuWidth/2);
+            var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth;
+            if (stickingOut > 0) {
+              menuLeftPos -= stickingOut + 10;
+            }
+            if (panelLeftPos + menuLeftPos < 0) {
+              menuLeftPos = 0;
+            }
+
+            var menuTemplate = createMenuTemplate($scope);
+            $menu = $(menuTemplate);
+            $menu.css('left', menuLeftPos);
+            $menu.mouseleave(function() {
+              dismiss(1000);
+            });
+
+            menuScope = $scope.$new();
+
+            $('.panel-menu').remove();
+            elem.append($menu);
+            $scope.$apply(function() {
+              $compile($menu.contents())(menuScope);
+            });
+
+            $(".panel-container").removeClass('panel-highlight');
+            $panelContainer.toggleClass('panel-highlight');
+
+            dismiss(2500);
+          };
+
+          elem.click(showMenu);
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});

+ 23 - 0
src/app/directives/tip.js

@@ -17,4 +17,27 @@ function (angular, kbn) {
         }
         }
       };
       };
     });
     });
+
+  angular
+    .module('grafana.directives')
+    .directive('editorOptBool', function($compile) {
+      return {
+        restrict: 'E',
+        link: function(scope, elem, attrs) {
+          var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
+          var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
+          var showIf = attrs.showIf ? (' ng-show="' + attrs.showIf + '" ') : '';
+
+          var template = '<div class="editor-option text-center"' + showIf + '>' +
+                         ' <label for="' + attrs.model + '" class="small">' +
+                           attrs.text + tip + '</label>' +
+                          '<input class="cr1" id="' + attrs.model + '" type="checkbox" ' +
+                          '       ng-model="' + attrs.model + '"' + ngchange +
+                          '       ng-checked="' + attrs.model + '"></input>' +
+                          ' <label for="' + attrs.model + '" class="cr1"></label>';
+          elem.replaceWith($compile(angular.element(template))(scope));
+        }
+      };
+    });
+
 });
 });

+ 12 - 41
src/app/panels/graph/axisEditor.html

@@ -40,42 +40,19 @@
 <div class="editor-row">
 <div class="editor-row">
   <div class="section">
   <div class="section">
     <h5>Legend styles</h5>
     <h5>Legend styles</h5>
-    <div class="editor-option">
-      <label class="small">Show Legend</label><input type="checkbox" ng-model="panel.legend.show" ng-checked="panel.legend.show" ng-change="render();">
-    </div>
-    <div class="editor-option">
-      <label class="small">Include Values</label><input type="checkbox" ng-model="panel.legend.values" ng-checked="panel.legend.values" ng-change="render();">
-    </div>
-    <div class="editor-option">
-      <label class="small">Align as table</label><input type="checkbox" ng-model="panel.legend.alignAsTable" ng-checked="panel.legend.alignAsTable">
-    </div>
-    <div class="editor-option">
-      <label class="small">Right side</label><input type="checkbox" ng-model="panel.legend.rightSide" ng-change="render();" ng-checked="panel.legend.rightSide">
-    </div>
+		<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="Right side" model="panel.legend.rightSide" change="render()"></editor-opt-bool>
   </div>
   </div>
 
 
   <div class="section" ng-if="panel.legend.values">
   <div class="section" ng-if="panel.legend.values">
     <h5>Legend values</h5>
     <h5>Legend values</h5>
-    <div class="editor-option">
-      <label class="small">Min</label><input type="checkbox" ng-model="panel.legend.min" ng-checked="panel.legend.min" ng-change="render();">
-    </div>
-
-    <div class="editor-option">
-      <label class="small">Max</label><input type="checkbox" ng-model="panel.legend.max" ng-checked="panel.legend.max" ng-change="render();">
-    </div>
-
-    <div class="editor-option">
-      <label class="small">Current</label><input type="checkbox" ng-model="panel.legend.current" ng-checked="panel.legend.current" ng-change="render();">
-    </div>
-
-    <div class="editor-option">
-      <label class="small">Total</label><input type="checkbox" ng-model="panel.legend.total" ng-checked="panel.legend.total" ng-change="render();">
-    </div>
-
-    <div class="editor-option">
-      <label class="small">Avg</label><input type="checkbox" ng-model="panel.legend.avg" ng-checked="panel.legend.avg" ng-change="render();">
-    </div>
-
+		<editor-opt-bool text="Min" model="panel.legend.min" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Max" model="panel.legend.max" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Current" model="panel.legend.current" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Total" model="panel.legend.total" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Avg" model="panel.legend.avg" change="render()"></editor-opt-bool>
   </div>
   </div>
 
 
   <div class="section">
   <div class="section">
@@ -96,19 +73,13 @@
       <label class="small">Color</label>
       <label class="small">Color</label>
       <spectrum-picker ng-model="panel.grid.threshold2Color" ng-change="render()" ></spectrum-picker>
       <spectrum-picker ng-model="panel.grid.threshold2Color" ng-change="render()" ></spectrum-picker>
     </div>
     </div>
-    <div class="editor-option">
-      <label class="small">Line mode</label><input type="checkbox" ng-model="panel.grid.thresholdLine" ng-checked="panel.grid.thresholdLine" ng-change="render();">
-    </div>
+		<editor-opt-bool text="Line mode" model="panel.grid.thresholdLine" change="render()"></editor-opt-bool>
   </div>
   </div>
 
 
   <div class="section">
   <div class="section">
     <h5>Show Axes</h5>
     <h5>Show Axes</h5>
-    <div class="editor-option">
-      <label class="small">X-Axis</label><input type="checkbox" ng-model="panel['x-axis']" ng-checked="panel['x-axis']" ng-change="render()">
-    </div>
-    <div class="editor-option">
-      <label class="small">Y-Axis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
-    </div>
+		<editor-opt-bool text="X-Axis" model="panel['x-axis']" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Y-axis" model="panel['y-axis']" change="render()"></editor-opt-bool>
   </div>
   </div>
 
 
 </div>
 </div>

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

@@ -8,7 +8,7 @@
           <span class="small" ng-show="datapointsOutside">Datapoints outside time range <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>
 
 
-        <div grafana-graph class="pointer histogram-chart">
+        <div grafana-graph class="histogram-chart">
         </div>
         </div>
 
 
       </div>
       </div>

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

@@ -15,7 +15,8 @@ define([
   'jquery.flot.selection',
   'jquery.flot.selection',
   'jquery.flot.time',
   'jquery.flot.time',
   'jquery.flot.stack',
   'jquery.flot.stack',
-  'jquery.flot.stackpercent'
+  'jquery.flot.stackpercent',
+  'jquery.flot.crosshair'
 ],
 ],
 function (angular, app, $, _, kbn, moment, TimeSeries) {
 function (angular, app, $, _, kbn, moment, TimeSeries) {
   'use strict';
   'use strict';
@@ -160,7 +161,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
 
 
       tooltip       : {
       tooltip       : {
         value_type: 'cumulative',
         value_type: 'cumulative',
-        query_as_alias: true
+        shared: false,
       },
       },
 
 
       targets: [{}],
       targets: [{}],

+ 17 - 19
src/app/panels/graph/styleEditor.html

@@ -1,15 +1,9 @@
 <div class="editor-row">
 <div class="editor-row">
   <div class="section">
   <div class="section">
     <h5>Chart Options</h5>
     <h5>Chart Options</h5>
-    <div class="editor-option">
-      <label class="small">Bars</label><input type="checkbox" ng-model="panel.bars" ng-checked="panel.bars" ng-change="render()">
-    </div>
-    <div class="editor-option">
-      <label class="small">Lines</label><input type="checkbox" ng-model="panel.lines" ng-checked="panel.lines" ng-change="render()">
-    </div>
-    <div class="editor-option">
-      <label class="small">Points</label><input type="checkbox" ng-model="panel.points" ng-checked="panel.points" ng-change="render()">
-    </div>
+		<editor-opt-bool text="Bars" model="panel.bars" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Lines" model="panel.lines" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Points" model="panel.points" change="render()"></editor-opt-bool>
   </div>
   </div>
 
 
   <div class="section">
   <div class="section">
@@ -30,19 +24,15 @@
       <label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
       <label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
       <select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
       <select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
     </div>
     </div>
-    <div class="editor-option">
-      <label class="small">Staircase line</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
-    </div>
+
+		<editor-opt-bool text="Staircase line" model="panel.steppedLine" change="render()"></editor-opt-bool>
   </div>
   </div>
   <div class="section">
   <div class="section">
     <h5>Multiple Series</h5>
     <h5>Multiple Series</h5>
-    <div class="editor-option">
-      <label class="small">Stack</label><input type="checkbox" ng-model="panel.stack" ng-checked="panel.stack" ng-change="render()">
-    </div>
-    <div class="editor-option" ng-show="panel.stack">
-      <label style="white-space:nowrap" class="small">Percent <tip>Stack as a percentage of total</tip></label>
-      <input type="checkbox"  ng-model="panel.percentage" ng-checked="panel.percentage" ng-change="render()">
-    </div>
+
+		<editor-opt-bool text="Stack" model="panel.stack" change="render()"></editor-opt-bool>
+		<editor-opt-bool text="Percent" model="panel.percentage" change="render()" tip="Stack as a percentage of total"></editor-opt-bool>
+
     <div class="editor-option" ng-show="panel.stack">
     <div class="editor-option" ng-show="panel.stack">
       <label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
       <label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
       <select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
       <select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
@@ -61,8 +51,16 @@
       <input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
       <input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
     </div>
     </div>
   </div>
   </div>
+
+  <div class="section">
+    <h5>Tooltip</h5>
+    <div class="editor-option">
+      <label class="small">shared <tip> Show all series values on the same time in the same tooltip and a x croshair to help follow all series</tip> </label><input type="checkbox" ng-model="panel.tooltip.shared" ng-checked="panel.tooltip.shared" ng-change="render()">
+    </div>
+  </div>
 </div>
 </div>
 
 
+
 <div class="editor-row">
 <div class="editor-row">
   <div class="section">
   <div class="section">
 		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
 		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>

+ 1 - 0
src/app/panels/text/module.js

@@ -20,6 +20,7 @@ function (angular, app, _, require) {
 
 
     // Set and populate defaults
     // Set and populate defaults
     var _d = {
     var _d = {
+      title: 'default title',
       mode    : "markdown", // 'html', 'markdown', 'text'
       mode    : "markdown", // 'html', 'markdown', 'text'
       content : "",
       content : "",
       style: {},
       style: {},

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

@@ -9,9 +9,7 @@
       border: 0px !important;
       border: 0px !important;
     }
     }
   </style>
   </style>
-  <!--  This is a complete hack. The form actually exists in the modal, but due to transclusion
-        $scope.input isn't available on the controller unless the form element is in this file -->
-  <form name="input" style="margin:3px 0 0 0">
+  <form name="input" style="margin:0">
     <ul class="nav nav-pills timepicker-dropdown">
     <ul class="nav nav-pills timepicker-dropdown">
       <li class="dropdown">
       <li class="dropdown">
 
 
@@ -47,6 +45,5 @@
         <a ng-click="timeSrv.refreshDashboard()"><i class="icon-refresh"></i></a>
         <a ng-click="timeSrv.refreshDashboard()"><i class="icon-refresh"></i></a>
       </li>
       </li>
     </ul>
     </ul>
-
   </form>
   </form>
 </div>
 </div>

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

@@ -79,7 +79,7 @@ function (angular, app, _, moment, kbn) {
         $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
         $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
       }
       }
 
 
-      $scope.emitAppEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });
+      $scope.appEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });
     };
     };
 
 
     // Constantly validate the input of the fields. This function does not change any date variables
     // Constantly validate the input of the fields. This function does not change any date variables

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

@@ -61,10 +61,7 @@
 					<label class="small">Icon size</label>
 					<label class="small">Icon size</label>
 					<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
 					<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
 				</div>
 				</div>
-				<div class="editor-option">
-					<label class="small">Grid line</label>
-					<input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
-				</div>
+				<editor-opt-bool text="Grid line" model="currentAnnotation.showLine"></editor-opt-bool>
 				<div class="editor-option">
 				<div class="editor-option">
 					<label class="small">Line color</label>
 					<label class="small">Line color</label>
 					<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
 					<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>

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

@@ -1,4 +1,4 @@
-<div ng-controller="DashboardCtrl" body-class class="dashboard" ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
+<div body-class class="dashboard" ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
 
 
 	<div ng-include="'app/partials/pro/dashboard_topnav.html'">
 	<div ng-include="'app/partials/pro/dashboard_topnav.html'">
 	</div>
 	</div>
@@ -74,9 +74,17 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<div style="padding-top:0px" ng-if="!row.collapse">
+				<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 class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
 					</div>
 					</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 -->
 					<!-- Panels -->
 					<div ng-repeat="(name, panel) in row.panels"
 					<div ng-repeat="(name, panel) in row.panels"

+ 7 - 15
src/app/partials/dasheditor.html

@@ -28,10 +28,7 @@
 						<label class="small">Time correction</label>
 						<label class="small">Time correction</label>
 						<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
 						<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
 					</div>
 					</div>
-					<div class="editor-option">
-						<label class="small">Hide controls (CTRL+H)</label>
-						<input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
-					</div>
+					<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
 				</div>
 				</div>
 			</div>
 			</div>
 			<div class="editor-row">
 			<div class="editor-row">
@@ -42,7 +39,6 @@
 						</bootstrap-tagsinput>
 						</bootstrap-tagsinput>
 						<tip>Press enter to a add tag</tip>
 						<tip>Press enter to a add tag</tip>
 					</div>
 					</div>
-
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -71,16 +67,12 @@
 		<div ng-if="editor.index == 2">
 		<div ng-if="editor.index == 2">
 			<div class="editor-row">
 			<div class="editor-row">
 				<div class="section">
 				<div class="section">
-					<div class="editor-option">
-						<label class="small">Templating</label>
-						<input type="checkbox" ng-model="dashboard.templating.enable" ng-checked="dashboard.templating.enable" ng-change="checkFeatureToggles()"x >
-					</div>
-					<div class="editor-option">
-						<label class="small">Annotations</label>
-						<input type="checkbox" ng-model="dashboard.annotations.enable" ng-checked="dashboard.annotations.enable" ng-change="checkFeatureToggles()">
-					</div>
-					<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
-						<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+					<editor-opt-bool text="Templating" model="dashboard.templating.enable"></editor-opt-bool>
+					<editor-opt-bool text="Annotations" model="dashboard.annotations.enable"></editor-opt-bool>
+					<div class="editor-option text-center" ng-repeat="pulldown in dashboard.nav">
+						<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label>
+						<input class="cr1" id="pulldown{{pulldown.type}}" type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+						<label for="pulldown{{pulldown.type}}" class="cr1"></label>
 					</div>
 					</div>
 				</div>
 				</div>
 			</div>
 			</div>

+ 2 - 0
src/app/partials/graphite/editor.html

@@ -76,6 +76,7 @@
 </div>
 </div>
 
 
 <section class="grafana-metric-options">
 <section class="grafana-metric-options">
+	<div class="grafana-target">
 		<div class="grafana-target-inner">
 		<div class="grafana-target-inner">
 			<ul class="grafana-segment-list">
 			<ul class="grafana-segment-list">
 				<li class="grafana-target-segment grafana-target-segment-icon">
 				<li class="grafana-target-segment grafana-target-segment-icon">
@@ -125,6 +126,7 @@
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
 		</div>
 		</div>
 	</div>
 	</div>
+ </div>
 </section>
 </section>
 
 
 <div class="editor-row">
 <div class="editor-row">

+ 8 - 4
src/app/partials/import.html

@@ -16,11 +16,15 @@
     </div>
     </div>
   </div>
   </div>
 
 
-  <div class="editor-row" style="margin-top: 10px;">
-    <table class="table table-condensed table-striped">
+  <div class="editor-row" style="margin-top: 10px;max-height: 400px; overflow-y: scroll;max-width: 500px;">
+    <table class="grafana-options-table">
       <tr ng-repeat="dash in dashboards">
       <tr ng-repeat="dash in dashboards">
-        <td style="padding-right: 20px;"><button class="btn btn-success" ng-click="import(dash.name)">Import</button>
-        <td style="width: 100%; vertical-align: middle;">{{dash.name}}</td>
+        <td style="">{{dash.name}}</td>
+				<td style="padding-left: 20px;">
+					<a class="pointer" ng-click="import(dash.name)">
+						import
+					</a>
+				</td>
       </tr>
       </tr>
     </table>
     </table>
   </div>
   </div>

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

@@ -22,7 +22,8 @@
 								{{dashboard.title}}
 								{{dashboard.title}}
 							</td>
 							</td>
 							<td style="text-align: center">
 							<td style="text-align: center">
-								<input type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
+								<input id="dash-{{$index}}" class="cr1" type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
+								<label for="dash-{{$index}}" class="cr1"></label>
 							</td>
 							</td>
 							<td style="text-align: center">
 							<td style="text-align: center">
 								<i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>
 								<i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>

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

@@ -20,12 +20,8 @@
     <div class="editor-option">
     <div class="editor-option">
       <label class="small">Height</label><input type="text" class="input-mini" ng-model='row.height'></input>
       <label class="small">Height</label><input type="text" class="input-mini" ng-model='row.height'></input>
     </div>
     </div>
-    <div class="editor-option">
-      <label class="small"> Editable </label><input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
-    </div>
-    <div class="editor-option">
-      <label class="small"> Show title </label><input type="checkbox" ng-model="row.showTitle" ng-checked="row.showTitle" />
-    </div>
+		<editor-opt-bool text="Editable" model="row.editable"></editor-opt-bool>
+		<editor-opt-bool text="Show title" model="row.showTitle"></editor-opt-bool>
 	</div>
 	</div>
   <div class="row-fluid" ng-if="editor.index == 1">
   <div class="row-fluid" ng-if="editor.index == 1">
     <div class="span12">
     <div class="span12">

+ 33 - 0
src/app/partials/share-panel.html

@@ -0,0 +1,33 @@
+<div ng-controller="SharePanelCtrl">
+	<div class="modal-header">
+		<div class="dashboard-editor-header">
+			<div class="dashboard-editor-title">
+				<i class="icon icon-share"></i>
+				Share
+			</div>
+
+			<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+				<div ng-repeat="tab in ['Link']" data-title="{{tab}}">
+				</div>
+			</div>
+
+		</div>
+	</div>
+
+	<div class="modal-body">
+
+		<div class="editor-row">
+			<editor-opt-bool text="Current time range" model="forCurrent" change="buildUrl()"></editor-opt-bool>
+			<editor-opt-bool text="To this panel only" model="toPanel" change="buildUrl()"></editor-opt-bool>
+			<editor-opt-bool text="Include template variables" model="includeTemplateVars" change="buildUrl()"></editor-opt-bool>
+		</div>
+
+		<div class="editor-row" style="margin-top: 20px;">
+			<input type="text" data-share-panel-url class="input input-fluid" ng-model='shareUrl'></input>
+		</div>
+	</div>
+
+	<div class="modal-footer">
+		<button class="btn btn-success pull-right" ng-click="dismiss();">close</button>
+	</div>
+</div>

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

@@ -17,9 +17,6 @@
 				</ul>
 				</ul>
 
 
 				<ul class="grafana-segment-list" ng-if="dashboard.templating.enable">
 				<ul class="grafana-segment-list" ng-if="dashboard.templating.enable">
-					<li class="small grafana-target-segment">
-						<strong>VARIABLES</strong>
-					</li>
 					<li ng-repeat-start="variable in variables" class="grafana-target-segment template-param-name">
 					<li ng-repeat-start="variable in variables" class="grafana-target-segment template-param-name">
 						<span class="template-variable ">
 						<span class="template-variable ">
 						${{variable.name}}:
 						${{variable.name}}:
@@ -31,10 +28,6 @@
 				</ul>
 				</ul>
 
 
 				<ul class="grafana-segment-list" ng-if="dashboard.annotations.enable">
 				<ul class="grafana-segment-list" ng-if="dashboard.annotations.enable">
-					<li class="small grafana-target-segment">
-						<strong>ANNOTATIONS</strong>
-					</li>
-
 					<li ng-repeat="annotation in dashboard.annotations.list" class="grafana-target-segment annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
 					<li ng-repeat="annotation in dashboard.annotations.list" class="grafana-target-segment annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
 						<a ng-click="disableAnnotation(annotation)">
 						<a ng-click="disableAnnotation(annotation)">
 							<i class="annotation-color-icon icon-bolt"></i>
 							<i class="annotation-color-icon icon-bolt"></i>

+ 6 - 12
src/app/partials/templating_editor.html

@@ -66,10 +66,10 @@
 							<label class="small">Datasource</label>
 							<label class="small">Datasource</label>
 							<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
 							<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
 						</div>
 						</div>
-						<div class="editor-option text-center" ng-show="current.type === 'query'">
-							<label class="small">Refresh on load <tip>Check if you want values to be updated on dashboard load, will slow down dashboard load time.</tip></label>
-							<input type="checkbox" ng-model="current.refresh" ng-checked="current.refresh">
-						</div>
+
+						<editor-opt-bool text="Refresh on load" show-if="current.type === 'query'"
+						                 tip="Check if you want values to be updated on dashboard load, will slow down dashboard load time"
+						                 model="current.refresh"></editor-opt-bool>
 					</div>
 					</div>
 
 
 					<div ng-show="current.type === 'interval'">
 					<div ng-show="current.type === 'interval'">
@@ -80,10 +80,7 @@
 							</div>
 							</div>
 						</div>
 						</div>
 						<div class="editor-row">
 						<div class="editor-row">
-							<div class="editor-option text-center">
-								<label class="small">Include auto interval</label>
-								<input type="checkbox" ng-model="current.auto" ng-checked="current.auto" ng-change="runQuery()">
-							</div>
+							<editor-opt-bool text="Include auto interval" model="current.auto" change="runQuery()"></editor-opt-bool>
 							<div class="editor-option" ng-show="current.auto">
 							<div class="editor-option" ng-show="current.auto">
 								<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
 								<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
 								<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
 								<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
@@ -118,10 +115,7 @@
 						</div>
 						</div>
 
 
 						<div class="editor-row" style="margin: 15px 0">
 						<div class="editor-row" style="margin: 15px 0">
-							<div class="editor-option text-center">
-								<label class="small">All option</label>
-								<input type="checkbox" ng-model="current.includeAll" ng-checked="current.includeAll" ng-change="runQuery()">
-							</div>
+							<editor-opt-bool text="All option" model="current.includeAll" change="runQuery()"></editor-opt-bool>
 							<div class="editor-option" ng-show="current.includeAll">
 							<div class="editor-option" ng-show="current.includeAll">
 								<label class="small">All format</label>
 								<label class="small">All format</label>
 								<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>
 								<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>

+ 3 - 4
src/app/partials/unsaved-changes.html

@@ -1,19 +1,18 @@
 <div class="modal-header">
 <div class="modal-header">
 </div>
 </div>
-<div class="modal-body">
 
 
-  <h3 class="text-center"><i class="icon-warning-sign"></i> Unsaved changes</h3>
+<div class="modal-body">
+  <h4 class="text-center"><i class="icon-warning-sign"></i> Unsaved changes</h4>
   <div class="row-fluid">
   <div class="row-fluid">
 		<span class="span3">
 		<span class="span3">
 			{{changes}}
 			{{changes}}
-
 		</span>
 		</span>
 	<button type="button" class="btn btn-success span2" ng-click="dismiss()">Cancel</button>
 	<button type="button" class="btn btn-success span2" ng-click="dismiss()">Cancel</button>
 	<button type="button" class="btn btn-success span2" ng-click="save();dismiss();">Save</button>
 	<button type="button" class="btn btn-success span2" ng-click="save();dismiss();">Save</button>
 	<button type="button" class="btn btn-warning span2" ng-click="ignore();dismiss();">Ignore</button>
 	<button type="button" class="btn btn-warning span2" ng-click="ignore();dismiss();">Ignore</button>
 	<span class="span3"></span>
 	<span class="span3"></span>
   </div>
   </div>
-
 </div>
 </div>
+
 <div class="modal-footer">
 <div class="modal-footer">
 </div>
 </div>

+ 23 - 6
src/app/routes/dashboard-from-db.js

@@ -22,7 +22,13 @@ function (angular) {
         templateUrl: 'app/partials/dashboard.html',
         templateUrl: 'app/partials/dashboard.html',
         controller : 'DashFromDBProvider',
         controller : 'DashFromDBProvider',
         reloadOnSearch: false,
         reloadOnSearch: false,
+      })
+      .when('/dashboard/import/:id', {
+        templateUrl: 'app/partials/dashboard.html',
+        controller : 'DashFromImportCtrl',
+        reloadOnSearch: false,
       });
       });
+
   });
   });
 
 
   module.controller('DashFromDBProvider', function($scope, $rootScope, datasourceSrv, $routeParams, alertSrv) {
   module.controller('DashFromDBProvider', function($scope, $rootScope, datasourceSrv, $routeParams, alertSrv) {
@@ -31,12 +37,23 @@ function (angular) {
     var isTemp = window.location.href.indexOf('dashboard/temp') !== -1;
     var isTemp = window.location.href.indexOf('dashboard/temp') !== -1;
 
 
     db.getDashboard($routeParams.id, isTemp)
     db.getDashboard($routeParams.id, isTemp)
-      .then(function(dashboard) {
-        $scope.emitAppEvent('setup-dashboard', dashboard);
-      }).then(null, function(error) {
-        $scope.emitAppEvent('setup-dashboard', { title: 'Grafana'});
-        alertSrv.set('Error', error, 'error');
-      });
+    .then(function(dashboard) {
+      $scope.initDashboard(dashboard, $scope);
+    }).then(null, function(error) {
+      $scope.initDashboard({ title: 'Grafana'}, $scope);
+      alertSrv.set('Error', error, 'error');
+    });
+  });
+
+  module.controller('DashFromImportCtrl', function($scope, $location, alertSrv) {
+
+    if (!window.grafanaImportDashboard) {
+      alertSrv.set('Not found', 'Cannot reload page with unsaved imported dashboard', 'warning', 7000);
+      $location.path('');
+      return;
+    }
+
+    $scope.initDashboard(window.grafanaImportDashboard, $scope);
   });
   });
 
 
 });
 });

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

@@ -52,7 +52,7 @@ function (angular, $, config, _) {
     };
     };
 
 
     file_load($routeParams.jsonFile).then(function(result) {
     file_load($routeParams.jsonFile).then(function(result) {
-      $scope.emitAppEvent('setup-dashboard', result);
+      $scope.initDashboard(result, $scope);
     });
     });
 
 
   });
   });

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

@@ -53,7 +53,7 @@ function (angular, $, config, _, kbn, moment) {
     };
     };
 
 
     script_load($routeParams.jsFile).then(function(result) {
     script_load($routeParams.jsFile).then(function(result) {
-      $scope.emitAppEvent('setup-dashboard', result.data);
+      $scope.initDashboard(result.data, $scope);
     });
     });
 
 
   });
   });

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

@@ -7,9 +7,21 @@ function (angular, _) {
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('alertSrv', function($timeout, $sce) {
+  module.service('alertSrv', function($timeout, $sce, $rootScope) {
     var self = this;
     var self = this;
 
 
+    this.init = function() {
+      $rootScope.onAppEvent('alert-error', function(e, alert) {
+        self.set(alert[0], alert[1], 'error');
+      });
+      $rootScope.onAppEvent('alert-warning', function(e, alert) {
+        self.set(alert[0], alert[1], 'warning', 5000);
+      });
+      $rootScope.onAppEvent('alert-success', function(e, alert) {
+        self.set(alert[0], alert[1], 'success', 3000);
+      });
+    };
+
     // List of all alert objects
     // List of all alert objects
     this.list = [];
     this.list = [];
 
 

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

@@ -1,5 +1,6 @@
 define([
 define([
   './alertSrv',
   './alertSrv',
+  './utilSrv',
   './datasourceSrv',
   './datasourceSrv',
   './timeSrv',
   './timeSrv',
   './templateSrv',
   './templateSrv',

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

@@ -13,7 +13,8 @@ define([
     var timezone;
     var timezone;
 
 
     this.init = function() {
     this.init = function() {
-      $rootScope.$on('refresh', this.clearCache);
+      $rootScope.onAppEvent('refresh', this.clearCache);
+      $rootScope.onAppEvent('setup-dashboard', this.clearCache);
     };
     };
 
 
     this.clearCache = function() {
     this.clearCache = function() {

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

@@ -22,7 +22,7 @@ function(angular, $) {
       });
       });
 
 
       keyboardManager.bind('ctrl+f', function() {
       keyboardManager.bind('ctrl+f', function() {
-        scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
+        scope.appEvent('show-dash-editor', { src: 'app/partials/search.html' });
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('ctrl+h', function() {
       keyboardManager.bind('ctrl+h', function() {
@@ -31,7 +31,7 @@ function(angular, $) {
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('ctrl+s', function(evt) {
       keyboardManager.bind('ctrl+s', function(evt) {
-        scope.emitAppEvent('save-dashboard', evt);
+        scope.appEvent('save-dashboard', evt);
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('ctrl+r', function() {
       keyboardManager.bind('ctrl+r', function() {
@@ -39,7 +39,7 @@ function(angular, $) {
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('ctrl+z', function(evt) {
       keyboardManager.bind('ctrl+z', function(evt) {
-        scope.emitAppEvent('zoom-out', evt);
+        scope.appEvent('zoom-out', evt);
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
       keyboardManager.bind('esc', function() {
       keyboardManager.bind('esc', function() {
@@ -53,7 +53,7 @@ function(angular, $) {
           modalData.$scope.dismiss();
           modalData.$scope.dismiss();
         }
         }
 
 
-        scope.emitAppEvent('hide-dash-editor');
+        scope.appEvent('hide-dash-editor');
 
 
         scope.exitFullscreen();
         scope.exitFullscreen();
       }, { inputDisabled: true });
       }, { inputDisabled: true });

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

@@ -25,7 +25,7 @@ function (angular, $, kbn, _, moment) {
       this.tags = data.tags || [];
       this.tags = data.tags || [];
       this.style = data.style || "dark";
       this.style = data.style || "dark";
       this.timezone = data.timezone || 'browser';
       this.timezone = data.timezone || 'browser';
-      this.editable = data.editable || true;
+      this.editable = data.editable === false ? false : true;
       this.hideControls = data.hideControls || false;
       this.hideControls = data.hideControls || false;
       this.rows = data.rows || [];
       this.rows = data.rows || [];
       this.nav = data.nav || [];
       this.nav = data.nav || [];

+ 4 - 0
src/app/services/elasticsearch/es-datasource.js

@@ -94,6 +94,10 @@ function (angular, _, config, kbn, moment) {
 
 
           for (var i = 0; i < fieldNames.length; i++) {
           for (var i = 0; i < fieldNames.length; i++) {
             fieldValue = fieldValue[fieldNames[i]];
             fieldValue = fieldValue[fieldNames[i]];
+            if (!fieldValue) {
+              console.log('could not find field in annotatation: ', fieldName);
+              return '';
+            }
           }
           }
 
 
           if (_.isArray(fieldValue)) {
           if (_.isArray(fieldValue)) {

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

@@ -203,7 +203,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       else {
       else {
         var self = this;
         var self = this;
         return this._influxRequest('POST', '/series', data).then(function() {
         return this._influxRequest('POST', '/series', data).then(function() {
-          self._removeUnslugifiedDashboard(title, false);
+          self._removeUnslugifiedDashboard(id, title, false);
           return { title: title, url: '/dashboard/db/' + id };
           return { title: title, url: '/dashboard/db/' + id };
         }, function(err) {
         }, function(err) {
           throw 'Failed to save dashboard to InfluxDB: ' + err.data;
           throw 'Failed to save dashboard to InfluxDB: ' + err.data;
@@ -211,11 +211,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       }
       }
     };
     };
 
 
-    InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, isTemp) {
+    InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, title, isTemp) {
+      if (id === title) { return; }
+
       var self = this;
       var self = this;
-      self._getDashboardInternal(id, isTemp).then(function(dashboard) {
+      self._getDashboardInternal(title, isTemp).then(function(dashboard) {
         if (dashboard !== null) {
         if (dashboard !== null) {
-          self.deleteDashboard(id);
+          self.deleteDashboard(title);
         }
         }
       });
       });
     };
     };

+ 34 - 39
src/app/services/panelSrv.js

@@ -10,70 +10,65 @@ function (angular, _) {
 
 
     this.init = function($scope) {
     this.init = function($scope) {
       if (!$scope.panel.span) { $scope.panel.span = 12; }
       if (!$scope.panel.span) { $scope.panel.span = 12; }
-      if (!$scope.panel.title) { $scope.panel.title = 'No title'; }
 
 
       var menu = [
       var menu = [
         {
         {
-          text: 'Edit',
-          configModal: "app/partials/paneleditor.html",
-          condition: !$scope.panelMeta.fullscreenEdit
-        },
-        {
-          text: 'Edit',
-          click: "toggleFullscreen(true)",
-          condition: $scope.panelMeta.fullscreenEdit
-        },
-        {
-          text: "Fullscreen",
+          text: "view",
+          icon: "icon-eye-open",
           click: 'toggleFullscreen(false)',
           click: 'toggleFullscreen(false)',
           condition: $scope.panelMeta.fullscreenView
           condition: $scope.panelMeta.fullscreenView
         },
         },
         {
         {
-          text: 'Duplicate',
-          click: 'duplicatePanel(panel)',
-          condition: true
+          text: 'edit',
+          icon: 'icon-cogs',
+          click: 'editPanel()',
+          condition: true,
         },
         },
         {
         {
-          text: 'Span',
-          submenu: [
-            { text: '1', click: 'updateColumnSpan(1)' },
-            { text: '2', click: 'updateColumnSpan(2)' },
-            { text: '3', click: 'updateColumnSpan(3)' },
-            { text: '4', click: 'updateColumnSpan(4)' },
-            { text: '5', click: 'updateColumnSpan(5)' },
-            { text: '6', click: 'updateColumnSpan(6)' },
-            { text: '7', click: 'updateColumnSpan(7)' },
-            { text: '8', click: 'updateColumnSpan(8)' },
-            { text: '9', click: 'updateColumnSpan(9)' },
-            { text: '10', click: 'updateColumnSpan(10)' },
-            { text: '11', click: 'updateColumnSpan(11)' },
-            { text: '12', click: 'updateColumnSpan(12)' },
-          ],
+          text: 'duplicate',
+          icon: 'icon-copy',
+          click: 'duplicatePanel(panel)',
           condition: true
           condition: true
         },
         },
         {
         {
-          text: 'Advanced',
-          submenu: [
-            { text: 'Panel JSON', click: 'editPanelJson()' },
-          ],
+          text: 'json',
+          icon: 'icon-code',
+          click: 'editPanelJson()',
           condition: true
           condition: true
         },
         },
         {
         {
-          text: 'Remove',
-          click: 'remove_panel_from_row(row, panel)',
+          text: 'share',
+          icon: 'icon-share',
+          click: 'sharePanel()',
           condition: true
           condition: true
-        }
+        },
       ];
       ];
 
 
       $scope.inspector = {};
       $scope.inspector = {};
       $scope.panelMeta.menu = _.where(menu, { condition: true });
       $scope.panelMeta.menu = _.where(menu, { condition: true });
 
 
+      $scope.editPanel = function() {
+        if ($scope.panelMeta.fullscreenEdit) {
+          $scope.toggleFullscreen(true);
+        }
+        else {
+          $scope.appEvent('show-dash-editor', { src: 'app/partials/paneleditor.html', scope: $scope });
+        }
+      };
+
+      $scope.sharePanel = function() {
+        $scope.appEvent('show-modal', {
+          src: './app/partials/share-panel.html',
+          scope: $scope.$new()
+        });
+      };
+
       $scope.editPanelJson = function() {
       $scope.editPanelJson = function() {
-        $scope.emitAppEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
+        $scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
       };
       };
 
 
       $scope.updateColumnSpan = function(span) {
       $scope.updateColumnSpan = function(span) {
-        $scope.panel.span = span;
+        $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12);
 
 
         $timeout(function() {
         $timeout(function() {
           $scope.$emit('render');
           $scope.$emit('render');

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

@@ -91,7 +91,7 @@ define([
         this.old_refresh = null;
         this.old_refresh = null;
       }
       }
 
 
-      $rootScope.emitAppEvent('time-range-changed', this.time);
+      $rootScope.appEvent('time-range-changed', this.time);
       $timeout(this.refreshDashboard, 0);
       $timeout(this.refreshDashboard, 0);
     };
     };
 
 

+ 31 - 0
src/app/services/utilSrv.js

@@ -0,0 +1,31 @@
+define([
+  'angular',
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('utilSrv', function($rootScope, $modal, $q) {
+
+    this.init = function() {
+      $rootScope.onAppEvent('show-modal', this.showModal);
+    };
+
+    this.showModal = function(e, options) {
+      var modal = $modal({
+        template: options.src,
+        persist: false,
+        show: false,
+        scope: options.scope,
+        keyboard: false
+      });
+
+      $q.when(modal).then(function(modalEl) {
+        modalEl.modal('show');
+      });
+    };
+
+  });
+
+});

+ 97 - 95
src/config.sample.js

@@ -2,108 +2,110 @@
 // config.js is where you will find the core Grafana configuration. This file contains parameter that
 // config.js is where you will find the core Grafana configuration. This file contains parameter that
 // must be set before Grafana is run for the first time.
 // must be set before Grafana is run for the first time.
 
 
-define(['settings'],
-function (Settings) {
+define(['settings'], function(Settings) {
   "use strict";
   "use strict";
 
 
   return new Settings({
   return new Settings({
 
 
-    /* Data sources
-    * ========================================================
-    * Datasources are used to fetch metrics, annotations, and serve as dashboard storage
-    *  - You can have multiple of the same type.
-    *  - grafanaDB: true    marks it for use for dashboard storage
-    *  - default: true      marks the datasource as the default metric source (if you have multiple)
-    *  - basic authentication: use url syntax http://username:password@domain:port
-    */
-
-    // InfluxDB example setup (the InfluxDB databases specified need to exist)
-    /*
-    datasources: {
-      influxdb: {
-        type: 'influxdb',
-        url: "http://my_influxdb_server:8086/db/database_name",
-        username: 'admin',
-        password: 'admin',
+      /* Data sources
+      * ========================================================
+      * Datasources are used to fetch metrics, annotations, and serve as dashboard storage
+      *  - You can have multiple of the same type.
+      *  - grafanaDB: true    marks it for use for dashboard storage
+      *  - default: true      marks the datasource as the default metric source (if you have multiple)
+      *  - basic authentication: use url syntax http://username:password@domain:port
+      */
+
+      // InfluxDB example setup (the InfluxDB databases specified need to exist)
+      /*
+      datasources: {
+        influxdb: {
+          type: 'influxdb',
+          url: "http://my_influxdb_server:8086/db/database_name",
+          username: 'admin',
+          password: 'admin',
+        },
+        grafana: {
+          type: 'influxdb',
+          url: "http://my_influxdb_server:8086/db/grafana",
+          username: 'admin',
+          password: 'admin',
+          grafanaDB: true
+        },
       },
       },
-      grafana: {
-        type: 'influxdb',
-        url: "http://my_influxdb_server:8086/db/grafana",
-        username: 'admin',
-        password: 'admin',
-        grafanaDB: true
+      */
+
+      // Graphite & Elasticsearch example setup
+      /*
+      datasources: {
+        graphite: {
+          type: 'graphite',
+          url: "http://my.graphite.server.com:8080",
+        },
+        elasticsearch: {
+          type: 'elasticsearch',
+          url: "http://my.elastic.server.com:9200",
+          index: 'grafana-dash',
+          grafanaDB: true,
+        }
       },
       },
-    },
-    */
-
-    // Graphite & Elasticsearch example setup
-    /*
-    datasources: {
-      graphite: {
-        type: 'graphite',
-        url: "http://my.graphite.server.com:8080",
+      */
+
+      // OpenTSDB & Elasticsearch example setup
+      /*
+      datasources: {
+        opentsdb: {
+          type: 'opentsdb',
+          url: "http://opentsdb.server:4242",
+        },
+        elasticsearch: {
+          type: 'elasticsearch',
+          url: "http://my.elastic.server.com:9200",
+          index: 'grafana-dash',
+          grafanaDB: true,
+        }
       },
       },
-      elasticsearch: {
-        type: 'elasticsearch',
-        url: "http://my.elastic.server.com:9200",
-        index: 'grafana-dash',
-        grafanaDB: true,
-      }
-    },
-    */
-
-    // OpenTSDB & Elasticsearch example setup
-    /*
-    datasources: {
-      opentsdb: {
-        type: 'opentsdb',
-        url: "http://opentsdb.server:4242",
+      */
+
+      /* Global configuration options
+      * ========================================================
+      */
+
+      // specify the limit for dashboard search results
+      search: {
+        max_results: 20
       },
       },
-      elasticsearch: {
-        type: 'elasticsearch',
-        url: "http://my.elastic.server.com:9200",
-        index: 'grafana-dash',
-        grafanaDB: true,
+
+      // default home dashboard
+      default_route: '/dashboard/file/default.json',
+
+      // set to false to disable unsaved changes warning
+      unsaved_changes_warning: true,
+
+      // set the default timespan for the playlist feature
+      // Example: "1m", "1h"
+      playlist_timespan: "1m",
+
+      // If you want to specify password before saving, please specify it below
+      // The purpose of this password is not security, but to stop some users from accidentally changing dashboards
+      admin: {
+        password: ''
+      },
+
+      // Change window title prefix from 'Grafana - <dashboard title>'
+      window_title_prefix: 'Grafana - ',
+
+      // Add your own custom panels
+      plugins: {
+        // list of plugin panels
+        panels: [],
+        // requirejs modules in plugins folder that should be loaded
+        // for example custom datasources
+        dependencies: [],
       }
       }
-    },
-    */
-
-    /* Global configuration options
-    * ========================================================
-    */
-
-    // specify the limit for dashboard search results
-    search: {
-      max_results: 20
-    },
-
-    // default home dashboard
-    default_route: '/dashboard/file/default.json',
-
-    // set to false to disable unsaved changes warning
-    unsaved_changes_warning: true,
-
-    // set the default timespan for the playlist feature
-    // Example: "1m", "1h"
-    playlist_timespan: "1m",
-
-    // If you want to specify password before saving, please specify it bellow
-    // The purpose of this password is not security, but to stop some users from accidentally changing dashboards
-    admin: {
-      password: ''
-    },
-
-    // Change window title prefix from 'Grafana - <dashboard title>'
-    window_title_prefix: 'Grafana - ',
-
-    // Add your own custom panels
-    plugins: {
-      // list of plugin panels
-      panels: [],
-      // requirejs modules in plugins folder that should be loaded
-      // for example custom datasources
-      dependencies: [],
-    }
-
-  });
+
+    });
 });
 });
+
+
+

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

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

+ 28 - 0
src/css/less/forms.less

@@ -0,0 +1,28 @@
+input[type=text].input-fluid {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 14px;
+  -moz-box-sizing: border-box;
+  height: 100%;
+}
+
+input[type="checkbox"].cr1 {
+  display: none;
+}
+
+input[type="checkbox"]+.cr1 {
+  display: inline-block;
+  height: 19px;
+  clear: none;
+  text-indent: 2px;
+  margin-top: 4px;
+  padding: 0 0 0 20px;
+  vertical-align:middle;
+  background: url(@checkboxImageUrl) left top no-repeat;
+  cursor:pointer;
+}
+
+input[type="checkbox"]:checked+label {
+  background: url(@checkboxImageUrl) 0px -18px no-repeat;
+}
+

+ 22 - 4
src/css/less/grafana.less

@@ -6,6 +6,7 @@
 @import "tables_lists.less";
 @import "tables_lists.less";
 @import "search.less";
 @import "search.less";
 @import "panel.less";
 @import "panel.less";
+@import "forms.less";
 
 
 .hide-controls {
 .hide-controls {
   padding: 0;
   padding: 0;
@@ -54,8 +55,12 @@
 }
 }
 
 
 .modal {
 .modal {
-  margin: 5%;
-  width: 90%;
+  max-width: 800px;
+  left: 0;
+  right: 0;
+  margin-left: auto;
+  margin-right: auto;
+  top: 200px;
 }
 }
 
 
 .grafana-search-metric-actions {
 .grafana-search-metric-actions {
@@ -118,8 +123,9 @@
 }
 }
 
 
 .dashboard-fullscreen {
 .dashboard-fullscreen {
-  .row-control-inner {
-    display: none;
+  .main-view-container {
+    height: 0;
+    overflow: hidden;
   }
   }
 }
 }
 
 
@@ -439,6 +445,18 @@ select.grafana-target-segment-input {
   max-width: 800px;
   max-width: 800px;
   max-height: 600px;
   max-height: 600px;
   overflow: hidden;
   overflow: hidden;
+  line-height: 14px;
+}
+
+
+
+.grafana-tooltip hr {
+ padding: 2px;
+ color: #c8c8c8;
+ margin: 0px;
+ border-bottom:0px solid #c8c8c8;
+ /*height:0px;
+ background-color: rgb(58, 57, 57);*/
 }
 }
 
 
 .tooltip.in {
 .tooltip.in {

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

@@ -1,5 +1,6 @@
 .graph-canvas-wrapper {
 .graph-canvas-wrapper {
   position: relative;
   position: relative;
+  cursor: crosshair;
 }
 }
 
 
 .graph-legend {
 .graph-legend {
@@ -124,7 +125,7 @@
 
 
 .graph-legend-series-hidden {
 .graph-legend-series-hidden {
   a {
   a {
-    color: darken(@linkColor, 45%);
+    color: @linkColorDisabled;
   }
   }
 }
 }
 
 
@@ -166,3 +167,18 @@
     float: left;
     float: left;
   }
   }
 }
 }
+
+.graph-tooltip {
+  .graph-tooltip-time {
+    text-align: center;
+    font-weight: bold;
+    position: relative;
+    top: -3px;
+  }
+
+  .graph-tooltip-value {
+    font-weight: bold;
+    float: right;
+    padding-left: 10px;
+  }
+}

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

@@ -252,11 +252,6 @@ form input.ng-invalid {
   max-width: 480px;
   max-width: 480px;
 }
 }
 
 
-.modal {
-  width: 100%;
-  top: 0px !important;
-}
-
 .tiny {
 .tiny {
   font-size: 50%;
   font-size: 50%;
 }
 }
@@ -567,12 +562,6 @@ div.flot-text {
   background-color: darken(@purple, 10%);
   background-color: darken(@purple, 10%);
 }
 }
 
 
-.annotation-editor-table {
-  td {
-    white-space: nowrap;
-  }
-}
-
 // Top menu
 // Top menu
 .save-dashboard-dropdown {
 .save-dashboard-dropdown {
   padding: 10px;
   padding: 10px;

+ 43 - 14
src/css/less/panel.less

@@ -15,10 +15,17 @@
   padding: 0px 10px 5px 10px;
   padding: 0px 10px 5px 10px;
 }
 }
 
 
+.panel-title-container {
+  min-height: 5px;
+  cursor: context-menu;
+}
+
 .panel-title {
 .panel-title {
   border: 0px;
   border: 0px;
   font-weight: bold;
   font-weight: bold;
   position: relative;
   position: relative;
+  font-size: 0.9em;
+  cursor: context-menu;
 }
 }
 
 
 .panel-loading {
 .panel-loading {
@@ -28,23 +35,10 @@
   z-index: 800;
   z-index: 800;
 }
 }
 
 
-.panel div.panel-extra div.panel-extra-container {
-  margin-right: -10px;
-  margin-top: 3px;
+.panel-header {
   text-align: center;
   text-align: center;
-  ul {
-    text-align: left;
-  }
-}
-
-.panel div.panel-extra {
-  font-size: 0.9em;
-  margin-bottom: 0px;
 }
 }
 
 
-.panel div.panel-extra .extra {
-  float:right !important;
-}
 
 
 .panel-error {
 .panel-error {
   color: @white;
   color: @white;
@@ -69,3 +63,38 @@
   bottom: 0;
   bottom: 0;
 }
 }
 
 
+.panel-menu {
+  z-index: 1000;
+  position: absolute;
+  background: @grafanaTargetFuncBackground;
+  border: 1px solid black;
+  top: -62px;
+
+  .panel-menu-row {
+    white-space: nowrap;
+    border-bottom: 1px solid black;
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+
+  .icon-move {
+    cursor: move;
+  }
+
+  .panel-menu-link, .panel-menu-icon {
+    padding: 5px 10px;
+  }
+
+  .panel-menu-link {
+    display: inline-block;
+    border-right: 1px solid black;
+    &:last-child {
+      border: none;
+    }
+  }
+}
+
+.panel-highlight  {
+  .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236, 0.8)");
+}

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

@@ -5,6 +5,6 @@
 }
 }
 
 
 .annotation-disabled, .annotation-disabled a {
 .annotation-disabled, .annotation-disabled a {
-  color: darken(@textColor, 25%);
+  color: @linkColorDisabled;
 }
 }
 
 

+ 4 - 2
src/css/less/variables.dark.less

@@ -52,6 +52,7 @@
 // Links
 // Links
 // -------------------------
 // -------------------------
 @linkColor:             darken(@white,11%);
 @linkColor:             darken(@white,11%);
+@linkColorDisabled:     darken(@linkColor,45%);
 @linkColorHover:        @white;
 @linkColorHover:        @white;
 
 
 
 
@@ -218,7 +219,7 @@
 @navbarLinkBackgroundHover:       transparent;
 @navbarLinkBackgroundHover:       transparent;
 @navbarLinkBackgroundActive:      @navbarBackground;
 @navbarLinkBackgroundActive:      @navbarBackground;
 
 
-@navbarBrandColor:                @navbarLinkColor;
+@navbarBrandColor:                @linkColor;
 
 
 // Inverted navbar
 // Inverted navbar
 @navbarInverseBackground:                #252A30;
 @navbarInverseBackground:                #252A30;
@@ -289,7 +290,8 @@
 @popoverArrowOuterWidth:  @popoverArrowWidth + 1;
 @popoverArrowOuterWidth:  @popoverArrowWidth + 1;
 @popoverArrowOuterColor:  rgba(0,0,0,.25);
 @popoverArrowOuterColor:  rgba(0,0,0,.25);
 
 
-
+// images
+@checkboxImageUrl: '../img/checkbox.png';
 
 
 // GRID
 // GRID
 // --------------------------------------------------
 // --------------------------------------------------

+ 4 - 3
src/css/less/variables.light.less

@@ -54,12 +54,13 @@
 // Scaffolding
 // Scaffolding
 // -------------------------
 // -------------------------
 @bodyBackground:        @grayLighter;
 @bodyBackground:        @grayLighter;
-@textColor:             #555;
+@textColor:             #666;
 
 
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
 @linkColor:             @textColor;
 @linkColor:             @textColor;
+@linkColorDisabled:     lighten(@linkColor,35%);
 @linkColorHover:        @blue;
 @linkColorHover:        @blue;
 
 
 
 
@@ -298,12 +299,12 @@
 @popoverArrowOuterWidth:  @popoverArrowWidth + 1;
 @popoverArrowOuterWidth:  @popoverArrowWidth + 1;
 @popoverArrowOuterColor:  rgba(0,0,0,.25);
 @popoverArrowOuterColor:  rgba(0,0,0,.25);
 
 
-
+// images
+@checkboxImageUrl: '../img/checkbox_white.png';
 
 
 // GRID
 // GRID
 // --------------------------------------------------
 // --------------------------------------------------
 
 
-
 // Default 940px grid
 // Default 940px grid
 // -------------------------
 // -------------------------
 @gridColumns:             12;
 @gridColumns:             12;

BIN
src/img/check_radio_sheet.png


BIN
src/img/checkbox.png


BIN
src/img/checkbox_white.png


+ 16 - 0
src/test/specs/dashboardSrv-specs.js

@@ -82,6 +82,22 @@ define([
 
 
   });
   });
 
 
+  describe('when creating dashboard with editable false', function() {
+    var model;
+
+    beforeEach(module('grafana.services'));
+    beforeEach(inject(function(dashboardSrv) {
+      model = dashboardSrv.create({
+        editable: false
+      });
+    }));
+
+    it('should set editable false', function() {
+      expect(model.editable).to.be(false);
+    });
+
+  });
+
   describe('when creating dashboard with old schema', function() {
   describe('when creating dashboard with old schema', function() {
     var model;
     var model;
     var graph;
     var graph;

+ 17 - 1
src/test/specs/grafanaGraph-specs.js

@@ -27,8 +27,12 @@ define([
               legend: {},
               legend: {},
               grid: {},
               grid: {},
               y_formats: [],
               y_formats: [],
-              seriesOverrides: []
+              seriesOverrides: [],
+	      tooltip: {
+                shared: true
+              }
             };
             };
+            scope.hiddenSeries = {};
             scope.dashboard = { timezone: 'browser' };
             scope.dashboard = { timezone: 'browser' };
             scope.range = {
             scope.range = {
               from: new Date('2014-08-09 10:00:00'),
               from: new Date('2014-08-09 10:00:00'),
@@ -145,6 +149,18 @@ define([
       });
       });
     });
     });
 
 
+    graphScenario('when series is hidden', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.hiddenSeries = {'series2': true};
+      });
+
+      it('should remove datapoints and disable stack', function() {
+        expect(ctx.plotData[0].info.alias).to.be('series1');
+        expect(ctx.plotData[1].data.length).to.be(0);
+        expect(ctx.plotData[1].stack).to.be(false);
+      });
+    });
+
   });
   });
 });
 });
 
 

+ 56 - 0
src/test/specs/graph-tooltip-specs.js

@@ -0,0 +1,56 @@
+define([
+  'jquery',
+  'directives/grafanaGraph.tooltip'
+], function($, tooltip) {
+  'use strict';
+
+  describe('graph tooltip', function() {
+    var elem = $('<div></div>');
+    var dashboard = {
+      formatDate: sinon.stub().returns('date'),
+    };
+    var scope =  {
+      panel: {
+        tooltip:  {
+          shared: true
+        },
+        y_formats: ['ms', 'none'],
+      }
+    };
+
+    var data = [
+      {
+        data: [[10,10], [12,20]],
+        info: { yaxis: 1 },
+        yaxis: { tickDecimals: 2 },
+      },
+      {
+        data: [[10,10], [12,20]],
+        info: { yaxis: 1 },
+        yaxis: { tickDecimals: 2 },
+      }
+    ];
+
+    var plot = {
+      getData: sinon.stub().returns(data),
+      highlight: sinon.stub(),
+      unhighlight: sinon.stub()
+    };
+
+    elem.data('plot', plot);
+
+    beforeEach(function() {
+      tooltip.register(elem, dashboard, scope);
+      elem.trigger('plothover', [{}, {x: 13}, {}]);
+    });
+
+    it('should add tooltip', function() {
+      var tooltipHtml = $(".graph-tooltip").text();
+      expect(tooltipHtml).to.be('date  : 40.00 ms : 20.00 ms');
+    });
+
+  });
+
+});
+
+

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

@@ -8,6 +8,7 @@ define([
     var self = this;
     var self = this;
 
 
     this.datasource = {};
     this.datasource = {};
+    this.$element = {};
     this.annotationsSrv = {};
     this.annotationsSrv = {};
     this.timeSrv = new TimeSrvStub();
     this.timeSrv = new TimeSrvStub();
     this.templateSrv = new TemplateSrvStub();
     this.templateSrv = new TemplateSrvStub();
@@ -16,18 +17,23 @@ define([
       get: function() { return self.datasource; }
       get: function() { return self.datasource; }
     };
     };
 
 
-    this.providePhase = function() {
+    this.providePhase = function(mocks) {
       return module(function($provide) {
       return module(function($provide) {
         $provide.value('datasourceSrv', self.datasourceSrv);
         $provide.value('datasourceSrv', self.datasourceSrv);
         $provide.value('annotationsSrv', self.annotationsSrv);
         $provide.value('annotationsSrv', self.annotationsSrv);
         $provide.value('timeSrv', self.timeSrv);
         $provide.value('timeSrv', self.timeSrv);
         $provide.value('templateSrv', self.templateSrv);
         $provide.value('templateSrv', self.templateSrv);
+        $provide.value('$element', self.$element);
+        _.each(mocks, function(key, value) {
+          $provide.value(key, value);
+        });
       });
       });
     };
     };
 
 
     this.createControllerPhase = function(controllerName) {
     this.createControllerPhase = function(controllerName) {
-      return inject(function($controller, $rootScope, $q) {
+      return inject(function($controller, $rootScope, $q, $location) {
         self.scope = $rootScope.$new();
         self.scope = $rootScope.$new();
+        self.$location = $location;
         self.scope.panel = {};
         self.scope.panel = {};
         self.scope.row = { panels:[] };
         self.scope.row = { panels:[] };
         self.scope.dashboard = {};
         self.scope.dashboard = {};
@@ -68,7 +74,7 @@ define([
         self.$httpBackend =  $httpBackend;
         self.$httpBackend =  $httpBackend;
 
 
         self.$rootScope.onAppEvent = function() {};
         self.$rootScope.onAppEvent = function() {};
-        self.$rootScope.emitAppEvent = function() {};
+        self.$rootScope.appEvent = function() {};
 
 
         self.service = $injector.get(name);
         self.service = $injector.get(name);
       });
       });

+ 19 - 64
src/test/specs/kbn-format-specs.js

@@ -3,76 +3,31 @@ define([
 ], function(kbn) {
 ], function(kbn) {
   'use strict';
   'use strict';
 
 
-  describe('millisecond formating', function() {
+  function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
 
 
-    it('should translate 4378634603 as 1.67 years', function() {
-      var str = kbn.msFormat(4378634603, 2);
-      expect(str).to.be('50.68 day');
+    describe('value format: ' + desc, function() {
+      it('should translate ' + value + ' as ' + result, function() {
+        var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
+        var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
+        expect(str).to.be(result);
+      });
     });
     });
 
 
-    it('should translate 3654454 as 1.02 hour', function() {
-      var str = kbn.msFormat(3654454, 2);
-      expect(str).to.be('1.02 hour');
-    });
-
-    it('should not downscale when value is zero', function() {
-      var str = kbn.msFormat(0, 2);
-      expect(str).to.be('0.00 ms');
-    });
-
-
-    it('should translate 365445 as 6.09 min', function() {
-      var str = kbn.msFormat(365445, 2);
-      expect(str).to.be('6.09 min');
-    });
-
-  });
-
-  describe('high negative exponent, issue #696', function() {
-    it('should ignore decimal correction if exponent', function() {
-      var str = kbn.getFormatFunction('')(2.75e-10, { tickDecimals: 12 });
-      expect(str).to.be('2.75e-10');
-    });
-    it('should format 0 correctly', function() {
-      var str = kbn.getFormatFunction('')(0.0, { tickDecimals: 12 });
-      expect(str).to.be('0');
-    });
-  });
+  }
 
 
-  describe('none format tests', function() {
-    it('should translate 2 as 2.0000 if axis decimals is 4', function() {
-      var str = kbn.getFormatFunction('')(2, { tickDecimals: 4 });
-      expect(str).to.be('2.0000');
-    });
-  });
-
-  describe('nanosecond formatting', function () {
-    it('should translate 25 to 25 ns', function () {
-      var str = kbn.nanosFormat(25, 2);
-      expect(str).to.be("25 ns");
-    });
-
-    it('should translate 2558 to 2.56 µs', function () {
-      var str = kbn.nanosFormat(2558, 2);
-      expect(str).to.be("2.56 µs");
-    });
+  describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
+  describeValueFormat('ms', 100, 1, 0, '100 ms');
+  describeValueFormat('ms', 1250, 10, 0, '1.25 s');
+  describeValueFormat('ms', 1250, 300, 0, '1.3 s');
+  describeValueFormat('ms', 65150, 10000, 0, '1.1 min');
+  describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour');
+  describeValueFormat('ms', 651500000, 150000000, 0, '8 day');
 
 
-    it('should translate 2558000 to 2.56 ms', function () {
-      var str = kbn.nanosFormat(2558000, 2);
-      expect(str).to.be("2.56 ms");
-    });
-
-    it('should translate 2019962000 to 2.02 s', function () {
-      var str = kbn.nanosFormat(2049962000, 2);
-      expect(str).to.be("2.05 s");
-    });
+  describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
+  describeValueFormat('none', 0, 0, 2, '0');
 
 
-    it('should translate 95199620000 to 1.59 m', function () {
-      var str = kbn.nanosFormat(95199620000, 2);
-      expect(str).to.be("1.59 m");
-    });
-
-  });
+  describeValueFormat('ns', 25, 1, 0, '25 ns');
+  describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
 
 
   describe('calculateInterval', function() {
   describe('calculateInterval', function() {
     it('1h 100 resultion', function() {
     it('1h 100 resultion', function() {

+ 71 - 0
src/test/specs/sharePanelCtrl-specs.js

@@ -0,0 +1,71 @@
+define([
+  './helpers',
+  'controllers/sharePanelCtrl'
+], function(helpers) {
+  'use strict';
+
+  describe('SharePanelCtrl', function() {
+    var ctx = new helpers.ControllerTestContext();
+
+    beforeEach(module('grafana.controllers'));
+
+    beforeEach(ctx.providePhase());
+    beforeEach(ctx.createControllerPhase('SharePanelCtrl'));
+
+    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' };
+
+        ctx.scope.buildUrl();
+        expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&panelId=22&fullscreen');
+      });
+
+      it('should generate share url absolute time', function() {
+        ctx.$location.path('/test');
+        ctx.scope.panel = { id: 22 };
+        ctx.timeSrv.time = { from: new Date(2012,1,1), to: new Date(2014,3,5) };
+
+        ctx.scope.buildUrl();
+        expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1328050800000&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: new Date(2012,1,1).toJSON(), to: new Date(2014,3,5).toJSON() };
+
+        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' };
+
+        ctx.scope.buildUrl();
+        expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now');
+      });
+
+      it('should include template variables in url', function() {
+        ctx.$location.path('/test');
+        ctx.scope.panel = { id: 22 };
+        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' };
+
+        ctx.scope.buildUrl();
+        expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&var-app=mupp&var-server=srv-01');
+      });
+
+    });
+
+  });
+
+});
+

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

@@ -43,6 +43,7 @@ require.config({
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
+    'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
   },
   },
@@ -77,6 +78,7 @@ require.config({
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
+    'jquery.flot.crosshair':['jquery', 'jquery.flot'],
 
 
     'angular-route':        ['angular'],
     'angular-route':        ['angular'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],
@@ -128,7 +130,9 @@ require([
     'specs/influxdb-datasource-specs',
     'specs/influxdb-datasource-specs',
     'specs/graph-ctrl-specs',
     'specs/graph-ctrl-specs',
     'specs/grafanaGraph-specs',
     'specs/grafanaGraph-specs',
+    'specs/graph-tooltip-specs',
     'specs/seriesOverridesCtrl-specs',
     'specs/seriesOverridesCtrl-specs',
+    'specs/sharePanelCtrl-specs',
     'specs/timeSrv-specs',
     'specs/timeSrv-specs',
     'specs/templateSrv-specs',
     'specs/templateSrv-specs',
     'specs/templateValuesSrv-specs',
     'specs/templateValuesSrv-specs',

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

@@ -435,7 +435,8 @@ angular.module('$strap.directives').factory('$modal', [
           return res.data;
           return res.data;
         })).then(function onSuccess(template) {
         })).then(function onSuccess(template) {
           var id = templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
           var id = templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
-          var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).addClass('fade').html(template);
+          // grafana change, removed fade
+          var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).html(template);
           if (options.modalClass)
           if (options.modalClass)
             $modal.addClass(options.modalClass);
             $modal.addClass(options.modalClass);
           $('body').append($modal);
           $('body').append($modal);

+ 1 - 1
src/vendor/bootstrap/less/modals.less

@@ -17,7 +17,7 @@
 
 
 .modal-backdrop,
 .modal-backdrop,
 .modal-backdrop.fade.in {
 .modal-backdrop.fade.in {
-  .opacity(80);
+  .opacity(70);
 }
 }
 
 
 // Base modal
 // Base modal

+ 176 - 0
src/vendor/jquery/jquery.flot.crosshair.js

@@ -0,0 +1,176 @@
+/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+The plugin supports these options:
+
+	crosshair: {
+		mode: null or "x" or "y" or "xy"
+		color: color
+		lineWidth: number
+	}
+
+Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
+crosshair that lets you trace the values on the x axis, "y" enables a
+horizontal crosshair and "xy" enables them both. "color" is the color of the
+crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
+the drawn lines (default is 1).
+
+The plugin also adds four public methods:
+
+  - setCrosshair( pos )
+
+    Set the position of the crosshair. Note that this is cleared if the user
+    moves the mouse. "pos" is in coordinates of the plot and should be on the
+    form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
+    axes), which is coincidentally the same format as what you get from a
+    "plothover" event. If "pos" is null, the crosshair is cleared.
+
+  - clearCrosshair()
+
+    Clear the crosshair.
+
+  - lockCrosshair(pos)
+
+    Cause the crosshair to lock to the current location, no longer updating if
+    the user moves the mouse. Optionally supply a position (passed on to
+    setCrosshair()) to move it to.
+
+    Example usage:
+
+	var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
+	$("#graph").bind( "plothover", function ( evt, position, item ) {
+		if ( item ) {
+			// Lock the crosshair to the data point being hovered
+			myFlot.lockCrosshair({
+				x: item.datapoint[ 0 ],
+				y: item.datapoint[ 1 ]
+			});
+		} else {
+			// Return normal crosshair operation
+			myFlot.unlockCrosshair();
+		}
+	});
+
+  - unlockCrosshair()
+
+    Free the crosshair to move again after locking it.
+*/
+
+(function ($) {
+    var options = {
+        crosshair: {
+            mode: null, // one of null, "x", "y" or "xy",
+            color: "rgba(170, 0, 0, 0.80)",
+            lineWidth: 1
+        }
+    };
+    
+    function init(plot) {
+        // position of crosshair in pixels
+        var crosshair = { x: -1, y: -1, locked: false };
+
+        plot.setCrosshair = function setCrosshair(pos) {
+            if (!pos)
+                crosshair.x = -1;
+            else {
+                var o = plot.p2c(pos);
+                crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
+                crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
+            }
+            
+            plot.triggerRedrawOverlay();
+        };
+        
+        plot.clearCrosshair = plot.setCrosshair; // passes null for pos
+        
+        plot.lockCrosshair = function lockCrosshair(pos) {
+            if (pos)
+                plot.setCrosshair(pos);
+            crosshair.locked = true;
+        };
+
+        plot.unlockCrosshair = function unlockCrosshair() {
+            crosshair.locked = false;
+        };
+
+        function onMouseOut(e) {
+            if (crosshair.locked)
+                return;
+
+            if (crosshair.x != -1) {
+                crosshair.x = -1;
+                plot.triggerRedrawOverlay();
+            }
+        }
+
+        function onMouseMove(e) {
+            if (crosshair.locked)
+                return;
+                
+            if (plot.getSelection && plot.getSelection()) {
+                crosshair.x = -1; // hide the crosshair while selecting
+                return;
+            }
+                
+            var offset = plot.offset();
+            crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
+            crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
+            plot.triggerRedrawOverlay();
+        }
+        
+        plot.hooks.bindEvents.push(function (plot, eventHolder) {
+            if (!plot.getOptions().crosshair.mode)
+                return;
+
+            eventHolder.mouseout(onMouseOut);
+            eventHolder.mousemove(onMouseMove);
+        });
+
+        plot.hooks.drawOverlay.push(function (plot, ctx) {
+            var c = plot.getOptions().crosshair;
+            if (!c.mode)
+                return;
+
+            var plotOffset = plot.getPlotOffset();
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            if (crosshair.x != -1) {
+                var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
+
+                ctx.strokeStyle = c.color;
+                ctx.lineWidth = c.lineWidth;
+                ctx.lineJoin = "round";
+
+                ctx.beginPath();
+                if (c.mode.indexOf("x") != -1) {
+                    var drawX = Math.floor(crosshair.x) + adj;
+                    ctx.moveTo(drawX, 0);
+                    ctx.lineTo(drawX, plot.height());
+                }
+                if (c.mode.indexOf("y") != -1) {
+                    var drawY = Math.floor(crosshair.y) + adj;
+                    ctx.moveTo(0, drawY);
+                    ctx.lineTo(plot.width(), drawY);
+                }
+                ctx.stroke();
+            }
+            ctx.restore();
+        });
+
+        plot.hooks.shutdown.push(function (plot, eventHolder) {
+            eventHolder.unbind("mouseout", onMouseOut);
+            eventHolder.unbind("mousemove", onMouseMove);
+        });
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'crosshair',
+        version: '1.0'
+    });
+})(jQuery);

+ 2 - 0
src/vendor/jquery/jquery.flot.js

@@ -1728,6 +1728,8 @@ Licensed under the MIT license.
             axis.delta = delta;
             axis.delta = delta;
             axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
             axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
             axis.tickSize = opts.tickSize || size;
             axis.tickSize = opts.tickSize || size;
+            // grafana addition
+            axis.scaledDecimals = axis.tickDecimals - Math.floor(Math.log(axis.tickSize) / Math.LN10);
 
 
             // Time mode was moved to a plug-in in 0.8, and since so many people use it
             // Time mode was moved to a plug-in in 0.8, and since so many people use it
             // we'll add an especially friendly reminder to make sure they included it.
             // we'll add an especially friendly reminder to make sure they included it.