diff --git a/plugins/datasource-zabbix/addMetricFunction.js b/plugins/datasource-zabbix/addMetricFunction.js new file mode 100644 index 0000000..4bc4014 --- /dev/null +++ b/plugins/datasource-zabbix/addMetricFunction.js @@ -0,0 +1,107 @@ +define([ + 'angular', + 'lodash', + 'jquery', + './metricFunctions' +], +function (angular, _, $, metricFunctions) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('addMetricFunction', function($compile) { + var inputTemplate = ''; + + var buttonTemplate = '' + + ''; + + 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 + "')", + }; + }) + }; + }); + } +}); diff --git a/plugins/datasource-zabbix/datasource.js b/plugins/datasource-zabbix/datasource.js index c610ee5..bfc4b24 100644 --- a/plugins/datasource-zabbix/datasource.js +++ b/plugins/datasource-zabbix/datasource.js @@ -9,7 +9,9 @@ define([ './helperFunctions', './dataProcessingService', './zabbixCache', - './queryCtrl' + './queryCtrl', + './addMetricFunction', + './metricFunctionEditor' ], function (angular, _, dateMath, utils) { 'use strict'; diff --git a/plugins/datasource-zabbix/metricFunctionEditor.js b/plugins/datasource-zabbix/metricFunctionEditor.js new file mode 100644 index 0000000..2b14d69 --- /dev/null +++ b/plugins/datasource-zabbix/metricFunctionEditor.js @@ -0,0 +1,244 @@ +define([ + 'angular', + 'lodash', + 'jquery', +], +function (angular, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('metricFunctionEditor', function($compile, templateSrv) { + + var funcSpanTemplate = '{{func.def.name}}('; + var paramTemplate = ''; + + var funcControlsTemplate = + '
' + + '' + + '' + + '' + + '' + + '
'; + + 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) { + $(', ').appendTo(elem); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + var $paramLink = $('' + paramValue + ''); + 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); + } + + }); + + $(')').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(); + } + }; + + }); + +}); diff --git a/plugins/datasource-zabbix/metricFunctions.js b/plugins/datasource-zabbix/metricFunctions.js new file mode 100644 index 0000000..b60854e --- /dev/null +++ b/plugins/datasource-zabbix/metricFunctions.js @@ -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; + } + }; + +}); diff --git a/plugins/datasource-zabbix/partials/query.editor.html b/plugins/datasource-zabbix/partials/query.editor.html index 3348f26..b5ff671 100644 --- a/plugins/datasource-zabbix/partials/query.editor.html +++ b/plugins/datasource-zabbix/partials/query.editor.html @@ -174,7 +174,11 @@ ng-style="target.item.style">
  • Options
  • -
  • + + +
  • + @@ -195,6 +199,16 @@
    +
    + +
    +
    +