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 @@
+
+