From 666d640216309e303c8df824dc713da80347514e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel.odegaard@gmail.com>
Date: Tue, 2 Sep 2014 12:55:45 +0200
Subject: [PATCH] Graphite: Metric node/segment selection is now a textbox with
 autocomplete dropdown, allow for custom glob expression for single node
 segment without enter text editor mode, Closes #281

---
 CHANGELOG.md                            |   1 +
 src/app/controllers/dashboardNavCtrl.js |   6 +-
 src/app/controllers/graphiteTarget.js   |   7 +-
 src/app/directives/all.js               |   1 +
 src/app/directives/graphiteSegment.js   | 137 ++++++++++++++++++++++++
 src/app/partials/graphite/editor.html   |  18 +---
 src/app/partials/influxdb/editor.html   |   1 -
 src/app/services/graphite/gfunc.js      |  28 +++--
 src/css/less/grafana.less               |   1 -
 src/css/less/graph.less                 |   4 +-
 src/test/specs/gfunc-specs.js           |   6 ++
 11 files changed, 176 insertions(+), 34 deletions(-)
 create mode 100644 src/app/directives/graphiteSegment.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index adf4db4a525..9102807b6e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
 
 **New features and improvements**
+- [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
 - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
 - [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
 - [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
diff --git a/src/app/controllers/dashboardNavCtrl.js b/src/app/controllers/dashboardNavCtrl.js
index 6f08ffb255e..204b874db70 100644
--- a/src/app/controllers/dashboardNavCtrl.js
+++ b/src/app/controllers/dashboardNavCtrl.js
@@ -81,8 +81,10 @@ function (angular, _, moment, config, store) {
         .then(function(result) {
           alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
 
-          $location.search({});
-          $location.path(result.url);
+          if (result.url !== $location.path()) {
+            $location.search({});
+            $location.path(result.url);
+          }
 
           $rootScope.$emit('dashboard-saved', $scope.dashboard);
 
diff --git a/src/app/controllers/graphiteTarget.js b/src/app/controllers/graphiteTarget.js
index aebc297b278..493f01eb562 100644
--- a/src/app/controllers/graphiteTarget.js
+++ b/src/app/controllers/graphiteTarget.js
@@ -168,17 +168,14 @@ function (angular, _, config, gfunc, Parser) {
         });
     };
 
-    $scope.setSegment = function (altIndex, segmentIndex) {
+    $scope.segmentValueChanged = function (segment, segmentIndex) {
       delete $scope.parserError;
 
-      $scope.segments[segmentIndex].value = $scope.altSegments[altIndex].value;
-      $scope.segments[segmentIndex].html = $scope.altSegments[altIndex].html;
-
       if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
         $scope.functions = [];
       }
 
-      if ($scope.altSegments[altIndex].expandable) {
+      if (segment.expandable) {
         return checkOtherSegments(segmentIndex + 1)
           .then(function () {
             setSegmentFocus(segmentIndex + 1);
diff --git a/src/app/directives/all.js b/src/app/directives/all.js
index f6051da8caa..35d718fc942 100644
--- a/src/app/directives/all.js
+++ b/src/app/directives/all.js
@@ -16,6 +16,7 @@ define([
   './addGraphiteFunc',
   './graphiteFuncEditor',
   './templateParamSelector',
+  './graphiteSegment',
   './grafanaVersionCheck',
   './influxdbFuncEditor'
 ], function () {});
diff --git a/src/app/directives/graphiteSegment.js b/src/app/directives/graphiteSegment.js
new file mode 100644
index 00000000000..0f1e4397d25
--- /dev/null
+++ b/src/app/directives/graphiteSegment.js
@@ -0,0 +1,137 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('graphiteSegment', function($compile, $sce) {
+      var inputTemplate = '<input type="text" data-provide="typeahead" ' +
+                            ' class="grafana-target-text-input input-medium"' +
+                            ' spellcheck="false" style="display:none"></input>';
+
+      var buttonTemplate = '<a class="grafana-target-segment" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
+
+      return {
+        link: function($scope, elem) {
+          var $input = $(inputTemplate);
+          var $button = $(buttonTemplate);
+          var segment = $scope.segment;
+          var options = null;
+          var cancelBlur = null;
+
+          $input.appendTo(elem);
+          $button.appendTo(elem);
+
+          $scope.updateVariableValue = function(value) {
+            if (value === '' || segment.value === value) {
+              return;
+            }
+
+            $scope.$apply(function() {
+              var selected = _.findWhere($scope.altSegments, { value: value });
+              if (selected) {
+                segment.value = selected.value;
+                segment.html = selected.html;
+                segment.expandable = selected.expandable;
+              }
+              else {
+                segment.value = value;
+                segment.html = $sce.trustAsHtml(value);
+                segment.expandable = true;
+              }
+              $scope.segmentValueChanged(segment, $scope.$index);
+            });
+          };
+
+          $scope.switchToLink = function(now) {
+            if (now === true || cancelBlur) {
+              clearTimeout(cancelBlur);
+              cancelBlur = null;
+              $input.hide();
+              $button.show();
+              $scope.updateVariableValue($input.val());
+            }
+            else {
+              // need to have long delay because the blur
+              // happens long before the click event on the typeahead options
+              cancelBlur = setTimeout($scope.switchToLink, 350);
+            }
+          };
+
+          $scope.source = function(query, callback) {
+            console.log("source!", callback);
+            if (options) {
+              return options;
+            }
+
+            $scope.$apply(function() {
+              $scope.getAltSegments($scope.$index).then(function() {
+                options = _.map($scope.altSegments, function(alt) { return alt.value; });
+
+                // add custom values
+                if (segment.value !== 'select metric' &&  _.indexOf(options, segment.value) === -1) {
+                  options.unshift(segment.value);
+                }
+
+                callback(options);
+              });
+            });
+          };
+
+          $scope.updater = function(value) {
+            if (value === segment.value) {
+              clearTimeout(cancelBlur);
+              $input.focus();
+              return value;
+            }
+
+            $input.val(value);
+            $scope.switchToLink(true);
+
+            return value;
+          };
+
+          $input.attr('data-provide', 'typeahead');
+          $input.typeahead({ source: $scope.source, minLength: 0, items: 100, updater: $scope.updater });
+
+          var typeahead = $input.data('typeahead');
+          typeahead.lookup = function () {
+            this.query = this.$element.val() || '';
+            var items = this.source(this.query, $.proxy(this.process, this));
+            return items ? this.process(items) : items;
+          };
+
+          $button.keydown(function(evt) {
+            // trigger typeahead on down arrow or enter key
+            if (evt.keyCode === 40 || evt.keyCode === 13) {
+              $button.click();
+            }
+          });
+
+          $button.click(function() {
+            options = null;
+            $input.css('width', ($button.width() + 16) + 'px');
+
+            $button.hide();
+            $input.show();
+            $input.focus();
+
+            var typeahead = $input.data('typeahead');
+            if (typeahead) {
+              $input.val('');
+              typeahead.lookup();
+            }
+          });
+
+          $input.blur($scope.switchToLink);
+
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});
diff --git a/src/app/partials/graphite/editor.html b/src/app/partials/graphite/editor.html
index b25478dba37..2a314615963 100755
--- a/src/app/partials/graphite/editor.html
+++ b/src/app/partials/graphite/editor.html
@@ -1,4 +1,3 @@
-
 <div class="editor-row" style="margin-top: 10px;">
 
 	<div  ng-repeat="target in panel.targets"
@@ -66,21 +65,10 @@
               ng-show="showTextEditor" />
 
       <ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
-        <li class="dropdown" ng-repeat="segment in segments" role="menuitem">
-          <a  tabindex="1"
-              class="grafana-target-segment dropdown-toggle"
-              data-toggle="dropdown"
-              ng-click="getAltSegments($index)"
-              focus-me="segment.focus"
-              ng-bind-html="segment.html">
-          </a>
-          <ul class="dropdown-menu scrollable grafana-segment-dropdown-menu" role="menu">
-            <li ng-repeat="altSegment in altSegments" role="menuitem">
-              <a href="javascript:void(0)" tabindex="1" ng-click="setSegment($index, $parent.$index)" ng-bind-html="altSegment.html"></a>
-            </li>
-          </ul>
+        <li ng-repeat="segment in segments" role="menuitem" graphite-segment>
+
         </li>
-        <li ng-repeat="func in functions">
+				<li ng-repeat="func in functions">
           <span graphite-func-editor class="grafana-target-segment grafana-target-function">
           </span>
         </li>
diff --git a/src/app/partials/influxdb/editor.html b/src/app/partials/influxdb/editor.html
index da0a525303a..995ce612d21 100644
--- a/src/app/partials/influxdb/editor.html
+++ b/src/app/partials/influxdb/editor.html
@@ -1,4 +1,3 @@
-
 <div class="editor-row" style="margin-top: 10px;">
 
   <div  ng-repeat="target in panel.targets"
diff --git a/src/app/services/graphite/gfunc.js b/src/app/services/graphite/gfunc.js
index ce0910326d8..2a379bdda73 100644
--- a/src/app/services/graphite/gfunc.js
+++ b/src/app/services/graphite/gfunc.js
@@ -58,13 +58,20 @@ function (_) {
   });
 
   addFuncDef({
-    name: 'sumSeries',
-    shortName: 'sum',
-    category: categories.Combine,
+    name: 'diffSeries',
+    category: categories.Calculate,
   });
 
   addFuncDef({
-    name: 'diffSeries',
+    name: 'asPercent',
+    params: [{ name: 'other', type: 'value_or_series', optional: true }],
+    defaultParams: ['$B'],
+    category: categories.Calculate,
+  });
+
+  addFuncDef({
+    name: 'sumSeries',
+    shortName: 'sum',
     category: categories.Combine,
   });
 
@@ -490,9 +497,16 @@ function (_) {
 
   FuncInstance.prototype.render = function(metricExp) {
     var str = this.def.name + '(';
-    var parameters = _.map(this.params, function(value) {
-      return _.isString(value) ? "'" + value + "'" : value;
-    });
+    var parameters = _.map(this.params, function(value, index) {
+
+      var paramType = this.def.params[index].type;
+      if (paramType === 'int' || paramType === 'value_or_series') {
+        return value;
+      }
+
+      return "'" + value + "'";
+
+    }, this);
 
     if (metricExp !== undefined) {
       parameters.unshift(metricExp);
diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less
index a0c2407a075..88160790fd2 100644
--- a/src/css/less/grafana.less
+++ b/src/css/less/grafana.less
@@ -216,7 +216,6 @@ input[type=text].grafana-function-param-input {
 }
 
 .grafana-target-controls {
-  width: 120px;
   float: right;
   list-style: none;
   margin: 0;
diff --git a/src/css/less/graph.less b/src/css/less/graph.less
index 8f049a44ae5..1bd424999ca 100644
--- a/src/css/less/graph.less
+++ b/src/css/less/graph.less
@@ -5,8 +5,6 @@
 .graph-legend {
   margin: 0 20px;
   text-align: center;
-  position: relative;
-  top: 2px;
 
   .popover-content {
     padding: 0;
@@ -45,7 +43,7 @@
 
 .graph-legend-series {
   padding-left: 10px;
-  padding-top: 2px;
+  padding-top: 6px;
 }
 
 .graph-legend-value {
diff --git a/src/test/specs/gfunc-specs.js b/src/test/specs/gfunc-specs.js
index cf547b52c35..fbedb378747 100644
--- a/src/test/specs/gfunc-specs.js
+++ b/src/test/specs/gfunc-specs.js
@@ -55,6 +55,12 @@ define([
       expect(func.render(undefined)).to.equal("randomWalk('test')");
     });
 
+    it('should handle function multiple series params', function() {
+      var func = gfunc.createFuncInstance('asPercent');
+      func.params[0] = '$B';
+      expect(func.render('$A')).to.equal("asPercent($A,$B)");
+    });
+
   });
 
   describe('when requesting function categories', function() {