Added initial metric functions directives based on graphite query

editor.
This commit is contained in:
Alexander Zobnin
2016-01-26 21:51:12 +03:00
parent 38f8e04d22
commit fcf0bdfaca
6 changed files with 597 additions and 19 deletions

View File

@@ -0,0 +1,107 @@
define([
'angular',
'lodash',
'jquery',
'./metricFunctions'
],
function (angular, _, $, metricFunctions) {
'use strict';
angular
.module('grafana.directives')
.directive('addMetricFunction', function($compile) {
var inputTemplate = '<input type="text"'+
' class="tight-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="tight-form-item tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<i class="fa fa-plus"></i></a>';
return {
link: function($scope, elem) {
var categories = metricFunctions.getCategories();
var allFunctions = getAllFunctionNames(categories);
$scope.functionMenu = createFunctionDropDownMenu(categories);
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: allFunctions,
minLength: 1,
items: 10,
updater: function (value) {
var funcDef = metricFunctions.getFuncDef(value);
if (!funcDef) {
// try find close match
value = value.toLowerCase();
funcDef = _.find(allFunctions, function(funcName) {
return funcName.toLowerCase().indexOf(value) === 0;
});
if (!funcDef) { return; }
}
$scope.$apply(function() {
$scope.addFunction(funcDef);
});
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
function getAllFunctionNames(categories) {
return _.reduce(categories, function(list, category) {
_.each(category, function(func) {
list.push(func.name);
});
return list;
}, []);
}
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
return {
text: category,
submenu: _.map(list, function(value) {
return {
text: value.name,
click: "addFunction('" + value.name + "')",
};
})
};
});
}
});

View File

@@ -9,7 +9,9 @@ define([
'./helperFunctions',
'./dataProcessingService',
'./zabbixCache',
'./queryCtrl'
'./queryCtrl',
'./addMetricFunction',
'./metricFunctionEditor'
],
function (angular, _, dateMath, utils) {
'use strict';

View File

@@ -0,0 +1,244 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('metricFunctionEditor', function($compile, templateSrv) {
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
var funcControlsTemplate =
'<div class="tight-form-func-controls">' +
'<span class="pointer fa fa-arrow-left"></span>' +
'<span class="pointer fa fa-question-circle"></span>' +
'<span class="pointer fa fa-remove" ></span>' +
'<span class="pointer fa fa-arrow-right"></span>' +
'</div>';
return {
restrict: 'A',
link: function postLink($scope, elem) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(funcControlsTemplate);
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
var paramCountAtLink = 0;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(func.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(function() {
relink();
scheduledRelink = false;
}, 200);
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || func.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
$scope.$apply($scope.targetChanged);
}
$input.hide();
$link.show();
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
var options = funcDef.params[paramIndex].options;
if (funcDef.params[paramIndex].type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function toggleFuncControls() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$funcControls.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$funcControls.show();
}
function addElementsAndCompile() {
$funcControls.appendTo(elem);
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
}
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $input = $(paramTemplate);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (funcDef.params[index].options) {
addTypeahead($input, index);
}
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFistParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(function() {
elem.find('.graphite-func-param-link').first().click();
}, 10);
}
}
function registerFuncControlsToggle() {
$funcLink.click(toggleFuncControls);
}
function registerFuncControlsActions() {
$funcControls.click(function(e) {
var $target = $(e.target);
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
$scope.removeFunction($scope.func);
});
return;
}
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move($scope.target.functions, $scope.$index, $scope.$index - 1);
$scope.targetChanged();
});
return;
}
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.target.functions, $scope.$index, $scope.$index + 1);
$scope.targetChanged();
});
return;
}
if ($target.hasClass('fa-question-circle')) {
window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
return;
}
});
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
}
};
});
});

View File

@@ -0,0 +1,189 @@
define([
'lodash',
'jquery'
],
function (_, $) {
'use strict';
var index = [];
var categories = {
Transform: [],
Aggregate: [],
Alias: []
};
function addFuncDef(funcDef) {
funcDef.params = funcDef.params || [];
funcDef.defaultParams = funcDef.defaultParams || [];
if (funcDef.category) {
funcDef.category.push(funcDef);
}
index[funcDef.name] = funcDef;
index[funcDef.shortName || funcDef.name] = funcDef;
}
var optionalSeriesRefArgs = [
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true }
];
addFuncDef({
name: 'scaleToSeconds',
category: categories.Transform,
params: [{ name: 'seconds', type: 'int' }],
defaultParams: [1],
});
addFuncDef({
name: 'groupBy',
category: categories.Transform,
params: [
{ name: 'interval', type: 'string'},
{ name: 'function', type: 'string', options: ['avg', 'min', 'max'] }
],
defaultParams: ['1m', 'avg'],
});
addFuncDef({
name: 'perSecond',
category: categories.Transform,
params: [{ name: "max value", type: "int", optional: true }],
defaultParams: [],
});
addFuncDef({
name: 'aggregate',
category: categories.Aggregate,
params: [
{ name: 'function', type: 'string', options: ['sum', 'avg', 'min', 'max'] }
],
defaultParams: ['sum'],
});
addFuncDef({
name: "holtWintersConfidenceBands",
category: categories.Aggregate,
params: [{ name: "delta", type: 'int' }],
defaultParams: [3]
});
addFuncDef({
name: 'alias',
category: categories.Alias,
params: [
{ name: 'alias', type: 'string'}
],
defaultParams: [],
});
addFuncDef({
name: 'averageSeries',
shortName: 'avg',
category: categories.Alias,
params: optionalSeriesRefArgs,
defaultParams: [''],
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
function FuncInstance(funcDef, options) {
this.def = funcDef;
this.params = [];
if (options && options.withDefaultParams) {
this.params = funcDef.defaultParams.slice(0);
}
this.updateText();
}
FuncInstance.prototype.render = function(metricExp) {
var str = this.def.name + '(';
var parameters = _.map(this.params, function(value, index) {
var paramType = this.def.params[index].type;
if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
return value;
}
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
return value;
}
return "'" + value + "'";
}, this);
if (metricExp) {
parameters.unshift(metricExp);
}
return str + parameters.join(', ') + ')';
};
FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
};
FuncInstance.prototype.updateParam = function(strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
this.params[index] = strValue;
}
this.updateText();
};
FuncInstance.prototype.updateText = function () {
if (this.params.length === 0) {
this.text = this.def.name + '()';
return;
}
var text = this.def.name + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
};
return {
createFuncInstance: function(funcDef, options) {
if (_.isString(funcDef)) {
if (!index[funcDef]) {
throw { message: 'Method not found ' + name };
}
funcDef = index[funcDef];
}
return new FuncInstance(funcDef, options);
},
getFuncDef: function(name) {
return index[name];
},
getCategories: function() {
return categories;
}
};
});

View File

@@ -174,7 +174,11 @@
ng-style="target.item.style">
</li>
<li class="tight-form-item query-keyword">Options</li>
<li class="dropdown" dropdown-typeahead="optionsMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
<li ng-repeat="func in target.functions">
<span metric-function-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" add-metric-function>
</li>
<!-- Scale -->
@@ -195,6 +199,16 @@
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-hide="target.mode == 1" ng-if="target.options">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 44px">&nbsp</li>
<!-- Select Application -->
<li class="tight-form-item query-keyword" style="width: 5em">Options</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-show="target.mode == 2">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 44px">&nbsp</li>

View File

@@ -1,9 +1,10 @@
define([
'angular',
'lodash',
'./metricFunctions',
'./utils'
],
function (angular, _, Utils) {
function (angular, _, metricFunctions, Utils) {
'use strict';
var module = angular.module('grafana.controllers');
@@ -16,22 +17,7 @@ define([
$scope.init = function () {
$scope.targetLetters = targetLetters;
$scope.metric = {};
$scope.optionsMenu = [
{text: "Transformations", submenu: [
{text: "groupBy", value: null},
{text: "scale", value: null}
]},
{text: "Aggregations", submenu: [
{text: "sum", value: null},
{text: "average", value: null},
{text: "min", value: null},
{text: "max", value: null}
]},
{text: "Alias", submenu: [
{text: "set alias", value: null},
]},
];
$scope.target.functions = [];
// Load default values
var targetDefaults = {
@@ -274,9 +260,45 @@ define([
// Parse target
};
$scope.targetChanged = function() {
console.log($scope.target.functions);
};
// Validate target and set validation info
$scope.validateTarget = function () {};
$scope.addFunction = function(funcDef) {
var newFunc = metricFunctions.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
$scope.target.functions.push(newFunc);
$scope.moveAliasFuncLast();
if (!newFunc.params.length && newFunc.added) {
$scope.targetChanged();
}
};
$scope.removeFunction = function(func) {
$scope.target.functions = _.without($scope.target.functions, func);
$scope.targetChanged();
};
$scope.moveAliasFuncLast = function() {
var aliasFunc = _.find($scope.target.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
$scope.target.functions = _.without($scope.target.functions, aliasFunc);
$scope.target.functions.push(aliasFunc);
}
};
$scope.functionChanged = function() {};
/**
* Switch query editor to specified mode.
* Modes: