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() {