diff --git a/.editorconfig b/.editorconfig index 84bbaf8a420..cbde126ca6c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 120 -insert_final_newline = true [*.go] indent_style = tab diff --git a/package.json b/package.json index 971e63053fc..06a83ecac18 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "clipboard": "^1.7.1", "d3": "^4.11.0", "d3-scale-chromatic": "^1.1.1", - "eventemitter3": "^2.0.2", + "eventemitter3": "^2.0.3", "file-saver": "^1.3.3", "jquery": "^3.2.1", "lodash": "^4.17.4", @@ -153,6 +153,7 @@ "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", + "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 1fa1dea4338..cf9c6b5efe0 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; -import $ from 'jquery'; import coreModule from '../../core_module'; function typeaheadMatcher(item) { var str = this.query; + if (str === '') { + return true; + } if (str[0] === '/') { str = str.substring(1); } @@ -30,6 +32,8 @@ export class FormDropdownCtrl { getOptions: any; optionCache: any; lookupText: boolean; + placeholder: any; + startOpen: any; /** @ngInject **/ constructor(private $scope, $element, private $sce, private templateSrv, private $q) { @@ -47,6 +51,10 @@ export class FormDropdownCtrl { this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; } + if (this.placeholder) { + this.inputElement.attr('placeholder', this.placeholder); + } + this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.typeahead({ source: this.typeaheadSource.bind(this), @@ -61,8 +69,7 @@ export class FormDropdownCtrl { var typeahead = this.inputElement.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; + this.source(this.query, this.process.bind(this)); }; this.linkElement.keydown(evt => { @@ -81,6 +88,10 @@ export class FormDropdownCtrl { }); this.inputElement.blur(this.inputBlur.bind(this)); + + if (this.startOpen) { + setTimeout(this.open.bind(this), 0); + } } getOptionsInternal(query) { @@ -121,9 +132,9 @@ export class FormDropdownCtrl { }); // add custom values - if (this.allowCustom) { + if (this.allowCustom && this.text !== '') { if (_.indexOf(optionTexts, this.text) === -1) { - options.unshift(this.text); + optionTexts.unshift(this.text); } } @@ -228,10 +239,10 @@ const template = ` style="display:none"> + tabindex="1" + ng-click="ctrl.open()" + give-focus="ctrl.focus" + ng-bind-html="ctrl.display || ' '"> `; @@ -250,6 +261,8 @@ export function formDropdownDirective() { allowCustom: '@', labelMode: '@', lookupText: '@', + placeholder: '@', + startOpen: '@', }, }; } diff --git a/public/app/core/directives/dropdown_typeahead.js b/public/app/core/directives/dropdown_typeahead.js index b44e953785e..25772b4638a 100644 --- a/public/app/core/directives/dropdown_typeahead.js +++ b/public/app/core/directives/dropdown_typeahead.js @@ -12,7 +12,7 @@ function (_, $, coreModule) { ' class="gf-form-input input-medium tight-form-input"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = ''; diff --git a/public/app/core/services/segment_srv.js b/public/app/core/services/segment_srv.js index d615525988a..6c126da1acd 100644 --- a/public/app/core/services/segment_srv.js +++ b/public/app/core/services/segment_srv.js @@ -106,10 +106,6 @@ function (angular, _, coreModule) { return new MetricSegment({fake: true, html: '', type: 'plus-button', cssClass: 'query-part' }); }; - this.newSelectTagValue = function() { - return new MetricSegment({value: 'select tag value', fake: true}); - }; - }); }); diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 2fb80a6ac50..d904aeb4789 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -16,12 +16,12 @@ Add variable
-
What does variables do?
-

Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names +

What do variables do?
+

Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard. - Checkout the + Check out the Templating documentation for more information. @@ -93,7 +93,7 @@

- Template names cannot begin with '__' that's reserved for Grafanas global variables + Template names cannot begin with '__', that's reserved for Grafana's global variables
diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index b4df928adca..1d72c2c11eb 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -1,46 +1,37 @@ -define([ - 'angular', - 'lodash', - 'jquery', - './gfunc', -], -function (angular, _, $, gfunc) { +define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) { 'use strict'; - gfunc = gfunc.default; + angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) { + var inputTemplate = + ''; - angular - .module('grafana.directives') - .directive('graphiteAddFunc', function($compile) { - var inputTemplate = ''; + var buttonTemplate = + '' + + ''; - var buttonTemplate = '' + - ''; + return { + link: function($scope, elem) { + var ctrl = $scope.ctrl; - return { - link: function($scope, elem) { - var ctrl = $scope.ctrl; - var graphiteVersion = ctrl.datasource.graphiteVersion; - var categories = gfunc.getCategories(graphiteVersion); - var allFunctions = getAllFunctionNames(categories); + var $input = $(inputTemplate); + var $button = $(buttonTemplate); - $scope.functionMenu = createFunctionDropDownMenu(categories); + $input.appendTo(elem); + $button.appendTo(elem); - var $input = $(inputTemplate); - var $button = $(buttonTemplate); - $input.appendTo(elem); - $button.appendTo(elem); + ctrl.datasource.getFuncDefs().then(function(funcDefs) { + var allFunctions = _.map(funcDefs, 'name').sort(); + + $scope.functionMenu = createFunctionDropDownMenu(funcDefs); $input.attr('data-provide', 'typeahead'); $input.typeahead({ source: allFunctions, minLength: 1, items: 10, - updater: function (value) { - var funcDef = gfunc.getFuncDef(value); + updater: function(value) { + var funcDef = ctrl.datasource.getFuncDef(value); if (!funcDef) { // try find close match value = value.toLowerCase(); @@ -48,7 +39,9 @@ function (angular, _, $, gfunc) { return funcName.toLowerCase().indexOf(value) === 0; }); - if (!funcDef) { return; } + if (!funcDef) { + return; + } } $scope.$apply(function() { @@ -57,7 +50,7 @@ function (angular, _, $, gfunc) { $input.trigger('blur'); return ''; - } + }, }); $button.click(function() { @@ -82,32 +75,81 @@ function (angular, _, $, gfunc) { }); $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) { - var submenu = _.map(list, function(value) { - return { - text: value.name, - click: "ctrl.addFunction('" + value.name + "')", + var drop; + var cleanUpDrop = function() { + if (drop) { + drop.destroy(); + drop = null; + } }; - }); - return { - text: category, - submenu: submenu - }; + $(elem) + .on('mouseenter', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + + var funcDef; + try { + funcDef = ctrl.datasource.getFuncDef($('a', this).text()); + } catch (e) { + // ignore + } + + if (funcDef && funcDef.description) { + var shortDesc = funcDef.description; + if (shortDesc.length > 500) { + shortDesc = shortDesc.substring(0, 497) + '...'; + } + + var contentElement = document.createElement('div'); + contentElement.innerHTML = '

' + funcDef.name + '

' + rst2html(shortDesc); + + drop = new Drop({ + target: this, + content: contentElement, + classes: 'drop-popover', + openOn: 'always', + tetherOptions: { + attachment: 'bottom left', + targetAttachment: 'bottom right', + }, + }); + } + }) + .on('mouseout', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + }); + + $scope.$on('$destroy', cleanUpDrop); + }, + }; + }); + + function createFunctionDropDownMenu(funcDefs) { + var categories = {}; + + _.forEach(funcDefs, function(funcDef) { + if (!funcDef.category) { + return; + } + if (!categories[funcDef.category]) { + categories[funcDef.category] = []; + } + categories[funcDef.category].push({ + text: funcDef.name, + click: "ctrl.addFunction('" + funcDef.name + "')", + }); }); + + return _.sortBy( + _.map(categories, function(submenu, category) { + return { + text: category, + submenu: _.sortBy(submenu, 'text'), + }; + }), + 'text' + ); } }); diff --git a/public/app/plugins/datasource/graphite/config_ctrl.ts b/public/app/plugins/datasource/graphite/config_ctrl.ts index 16a25bacbe5..b22a860315c 100644 --- a/public/app/plugins/datasource/graphite/config_ctrl.ts +++ b/public/app/plugins/datasource/graphite/config_ctrl.ts @@ -8,7 +8,6 @@ export class GraphiteConfigCtrl { this.datasourceSrv = datasourceSrv; this.current.jsonData = this.current.jsonData || {}; this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9'; - this.autoDetectGraphiteVersion(); } diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 38133f801d8..f02945b8969 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; +import gfunc from './gfunc'; /** @ngInject */ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) { @@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv this.cacheTimeout = instanceSettings.cacheTimeout; this.withCredentials = instanceSettings.withCredentials; this.render_method = instanceSettings.render_method || 'POST'; + this.funcDefs = null; + this.funcDefsPromise = null; this.getQueryOptionsInfo = function() { return { @@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv let options = optionalOptions || {}; let interpolatedQuery = templateSrv.replace(query); + // special handling for tag_values([,]*), this is used for template variables + let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/); + if (matches) { + const expressions = []; + const exprRegex = /, *([^,]+)/g; + let match; + while ((match = exprRegex.exec(matches[2])) !== null) { + expressions.push(match[1]); + } + options.limit = 10000; + return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options); + } + + // special handling for tags([,]*), this is used for template variables + matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/); + if (matches) { + const expressions = []; + if (matches[1]) { + expressions.push(matches[1]); + const exprRegex = /, *([^,]+)/g; + let match; + while ((match = exprRegex.exec(matches[2])) !== null) { + expressions.push(match[1]); + } + } + options.limit = 10000; + return this.getTagsAutoComplete(expressions, undefined, options); + } + let httpOptions: any = { method: 'GET', url: '/metrics/find', @@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv requestId: options.requestId, }; - if (options && options.range) { + if (options.range) { httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.until = this.translateTime(options.range.to, true); } @@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv requestId: options.requestId, }; - if (options && options.range) { + if (options.range) { httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.until = this.translateTime(options.range.to, true); } @@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv let httpOptions: any = { method: 'GET', - url: '/tags/' + tag, + url: '/tags/' + templateSrv.replace(tag), // for cancellations requestId: options.requestId, }; - if (options && options.range) { + if (options.range) { httpOptions.params.from = this.translateTime(options.range.from, false); httpOptions.params.until = this.translateTime(options.range.to, true); } @@ -279,18 +311,29 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; - this.getTagsAutoComplete = (expression, tagPrefix) => { + this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => { + let options = optionalOptions || {}; + let httpOptions: any = { method: 'GET', url: '/tags/autoComplete/tags', params: { - expr: expression, + expr: _.map(expressions, expression => templateSrv.replace(expression)), }, + // for cancellations + requestId: options.requestId, }; if (tagPrefix) { httpOptions.params.tagPrefix = tagPrefix; } + if (options.limit) { + httpOptions.params.limit = options.limit; + } + if (options.range) { + httpOptions.params.from = this.translateTime(options.range.from, false); + httpOptions.params.until = this.translateTime(options.range.to, true); + } return this.doGraphiteRequest(httpOptions).then(results => { if (results.data) { @@ -303,19 +346,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; - this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => { + this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => { + let options = optionalOptions || {}; + let httpOptions: any = { method: 'GET', url: '/tags/autoComplete/values', params: { - expr: expression, - tag: tag, + expr: _.map(expressions, expression => templateSrv.replace(expression)), + tag: templateSrv.replace(tag), }, + // for cancellations + requestId: options.requestId, }; if (valuePrefix) { httpOptions.params.valuePrefix = valuePrefix; } + if (options.limit) { + httpOptions.params.limit = options.limit; + } + if (options.range) { + httpOptions.params.from = this.translateTime(options.range.from, false); + httpOptions.params.until = this.translateTime(options.range.to, true); + } return this.doGraphiteRequest(httpOptions).then(results => { if (results.data) { @@ -328,10 +382,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; - this.getVersion = function() { + this.getVersion = function(optionalOptions) { + let options = optionalOptions || {}; + let httpOptions = { method: 'GET', - url: '/version/_', // Prevent last / trimming + url: '/version', + requestId: options.requestId, }; return this.doGraphiteRequest(httpOptions) @@ -347,6 +404,52 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; + this.createFuncInstance = function(funcDef, options?) { + return gfunc.createFuncInstance(funcDef, options, this.funcDefs); + }; + + this.getFuncDef = function(name) { + return gfunc.getFuncDef(name, this.funcDefs); + }; + + this.waitForFuncDefsLoaded = function() { + return this.getFuncDefs(); + }; + + this.getFuncDefs = function() { + if (this.funcDefsPromise !== null) { + return this.funcDefsPromise; + } + + if (!supportsFunctionIndex(this.graphiteVersion)) { + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + this.funcDefsPromise = Promise.resolve(this.funcDefs); + return this.funcDefsPromise; + } + + let httpOptions = { + method: 'GET', + url: '/functions', + }; + + this.funcDefsPromise = this.doGraphiteRequest(httpOptions) + .then(results => { + if (results.status !== 200 || typeof results.data !== 'object') { + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + } else { + this.funcDefs = gfunc.parseFuncDefs(results.data); + } + return this.funcDefs; + }) + .catch(err => { + console.log('Fetching graphite functions error', err); + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + return this.funcDefs; + }); + + return this.funcDefsPromise; + }; + this.testDatasource = function() { return this.metricFindQuery('*').then(function() { return { status: 'success', message: 'Data source is working' }; @@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv function supportsTags(version: string): boolean { return isVersionGtOrEq(version, '1.1'); } + +function supportsFunctionIndex(version: string): boolean { + return isVersionGtOrEq(version, '1.1'); +} diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 534b0886fff..949a6912720 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -2,17 +2,18 @@ define([ 'angular', 'lodash', 'jquery', + 'rst2html', ], -function (angular, _, $) { +function (angular, _, $, rst2html) { 'use strict'; angular .module('grafana.directives') - .directive('graphiteFuncEditor', function($compile, templateSrv) { + .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) { var funcSpanTemplate = '{{func.def.name}}('; var paramTemplate = ''; + ' class="input-small tight-form-func-param">'; var funcControlsTemplate = '
' + @@ -29,19 +30,20 @@ function (angular, _, $) { var $funcControls = $(funcControlsTemplate); var ctrl = $scope.ctrl; var func = $scope.func; - var funcDef = func.def; var scheduledRelink = false; var paramCountAtLink = 0; + var cancelBlur = null; function clickFuncParam(paramIndex) { /*jshint validthis:true */ var $link = $(this); + var $comma = $link.prev('.comma'); var $input = $link.next(); $input.val(func.params[paramIndex]); - $input.css('width', ($link.width() + 16) + 'px'); + $comma.removeClass('last'); $link.hide(); $input.show(); $input.focus(); @@ -68,31 +70,64 @@ function (angular, _, $) { } } - function inputBlur(paramIndex) { + function paramDef(index) { + if (index < func.def.params.length) { + return func.def.params[index]; + } + if (_.last(func.def.params).multiple) { + return _.assign({}, _.last(func.def.params), {optional: true}); + } + return {}; + } + + function switchToLink(inputElem, paramIndex) { /*jshint validthis:true */ - var $input = $(this); + var $input = $(inputElem); + + clearTimeout(cancelBlur); + cancelBlur = null; + var $link = $input.prev(); + var $comma = $link.prev('.comma'); var newValue = $input.val(); - if (newValue !== '' || func.def.params[paramIndex].optional) { - $link.html(templateSrv.highlightVariablesAsHtml(newValue)); - - func.updateParam($input.val(), paramIndex); - scheduledRelinkIfNeeded(); - - $scope.$apply(function() { - ctrl.targetChanged(); - }); - - $input.hide(); - $link.show(); + // remove optional empty params + if (newValue !== '' || paramDef(paramIndex).optional) { + func.updateParam(newValue, paramIndex); + $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); } + + scheduledRelinkIfNeeded(); + + $scope.$apply(function() { + ctrl.targetChanged(); + }); + + if ($link.hasClass('last') && newValue === '') { + $comma.addClass('last'); + } else { + $link.removeClass('last'); + } + + $input.hide(); + $link.show(); + } + + // this = input element + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var inputElem = this; + // happens long before the click event on the typeahead options + // need to have long delay because the blur + cancelBlur = setTimeout(function() { + switchToLink(inputElem, paramIndex); + }, 200); } function inputKeyPress(paramIndex, e) { /*jshint validthis:true */ if(e.which === 13) { - inputBlur.call(this, paramIndex); + $(this).blur(); } } @@ -104,8 +139,8 @@ function (angular, _, $) { function addTypeahead($input, paramIndex) { $input.attr('data-provide', 'typeahead'); - var options = funcDef.params[paramIndex].options; - if (funcDef.params[paramIndex].type === 'int') { + var options = paramDef(paramIndex).options; + if (paramDef(paramIndex).type === 'int') { options = _.map(options, function(val) { return val.toString(); }); } @@ -114,9 +149,8 @@ function (angular, _, $) { minLength: 0, items: 20, updater: function (value) { - setTimeout(function() { - inputBlur.call($input[0], paramIndex); - }, 0); + $input.val(value); + switchToLink($input[0], paramIndex); return value; } }); @@ -148,18 +182,34 @@ function (angular, _, $) { $funcControls.appendTo(elem); $funcLink.appendTo(elem); - _.each(funcDef.params, function(param, index) { - if (param.optional && func.params.length <= index) { - return; - } + var defParams = _.clone(func.def.params); + var lastParam = _.last(func.def.params); - if (index > 0) { - $(', ').appendTo(elem); + while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { + defParams.push(_.assign({}, lastParam, {optional: true})); + } + + _.each(defParams, function(param, index) { + if (param.optional && func.params.length < index) { + return false; } var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); - var $paramLink = $('' + paramValue + ''); + + var last = (index >= func.params.length - 1) && param.optional && !paramValue; + if (last && param.multiple) { + paramValue = '+'; + } + + if (index > 0) { + $(', ').appendTo(elem); + } + + var $paramLink = $( + '' + + (paramValue || ' ') + ''); var $input = $(paramTemplate); + $input.attr('placeholder', param.name); paramCountAtLink++; @@ -171,10 +221,9 @@ function (angular, _, $) { $input.keypress(_.partial(inputKeyPress, index)); $paramLink.click(_.partial(clickFuncParam, index)); - if (funcDef.params[index].options) { + if (param.options) { addTypeahead($input, index); } - }); $(')').appendTo(elem); @@ -182,7 +231,7 @@ function (angular, _, $) { $compile(elem.contents())($scope); } - function ifJustAddedFocusFistParam() { + function ifJustAddedFocusFirstParam() { if ($scope.func.added) { $scope.func.added = false; setTimeout(function() { @@ -223,7 +272,20 @@ function (angular, _, $) { } if ($target.hasClass('fa-question-circle')) { - window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank'); + var funcDef = ctrl.datasource.getFuncDef(func.def.name); + if (funcDef && funcDef.description) { + popoverSrv.show({ + element: e.target, + position: 'bottom left', + classNames: 'drop-popover drop-function-def', + template: '
' + + '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', + openOn: 'click', + }); + } else { + window.open( + "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank'); + } return; } }); @@ -233,7 +295,7 @@ function (angular, _, $) { elem.children().remove(); addElementsAndCompile(); - ifJustAddedFocusFistParam(); + ifJustAddedFocusFirstParam(); registerFuncControlsToggle(); registerFuncControlsActions(); } diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index 282de306518..3d33d0f1005 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -1,71 +1,56 @@ import _ from 'lodash'; -import $ from 'jquery'; import { isVersionGtOrEq } from 'app/core/utils/version'; -var index = []; -var categories = { - Combine: [], - Transform: [], - Calculate: [], - Filter: [], - Special: [], -}; +var index = {}; 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; + if (funcDef.shortName) { + index[funcDef.shortName] = 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 }, -]; +var optionalSeriesRefArgs = [{ name: 'other', type: 'value_or_series', optional: true, multiple: true }]; addFuncDef({ name: 'scaleToSeconds', - category: categories.Transform, + category: 'Transform', params: [{ name: 'seconds', type: 'int' }], defaultParams: [1], }); addFuncDef({ name: 'perSecond', - category: categories.Transform, + category: 'Transform', params: [{ name: 'max value', type: 'int', optional: true }], defaultParams: [], }); addFuncDef({ name: 'holtWintersForecast', - category: categories.Calculate, + category: 'Calculate', }); addFuncDef({ name: 'holtWintersConfidenceBands', - category: categories.Calculate, + category: 'Calculate', params: [{ name: 'delta', type: 'int' }], defaultParams: [3], }); addFuncDef({ name: 'holtWintersAberration', - category: categories.Calculate, + category: 'Calculate', params: [{ name: 'delta', type: 'int' }], defaultParams: [3], }); addFuncDef({ name: 'nPercentile', - category: categories.Calculate, + category: 'Calculate', params: [{ name: 'Nth percentile', type: 'int' }], defaultParams: [95], }); @@ -74,48 +59,48 @@ addFuncDef({ name: 'diffSeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Combine', }); addFuncDef({ name: 'stddevSeries', params: optionalSeriesRefArgs, defaultParams: [''], - category: categories.Calculate, + category: 'Combine', }); addFuncDef({ name: 'divideSeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Combine', }); addFuncDef({ name: 'multiplySeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Combine', }); addFuncDef({ name: 'asPercent', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Combine', }); addFuncDef({ name: 'group', params: optionalSeriesRefArgs, defaultParams: ['#A', '#B'], - category: categories.Combine, + category: 'Combine', }); addFuncDef({ name: 'sumSeries', shortName: 'sum', - category: categories.Combine, + category: 'Combine', params: optionalSeriesRefArgs, defaultParams: [''], }); @@ -123,78 +108,66 @@ addFuncDef({ addFuncDef({ name: 'averageSeries', shortName: 'avg', - category: categories.Combine, + category: 'Combine', params: optionalSeriesRefArgs, defaultParams: [''], }); addFuncDef({ name: 'rangeOfSeries', - category: categories.Combine, + category: 'Combine', }); addFuncDef({ name: 'percentileOfSeries', - category: categories.Combine, + category: 'Combine', params: [{ name: 'n', type: 'int' }, { name: 'interpolate', type: 'boolean', options: ['true', 'false'] }], defaultParams: [95, 'false'], }); addFuncDef({ name: 'sumSeriesWithWildcards', - category: categories.Combine, - params: [ - { name: 'node', type: 'int' }, - { name: 'node', type: 'int', optional: true }, - { name: 'node', type: 'int', optional: true }, - { name: 'node', type: 'int', optional: true }, - ], + category: 'Combine', + params: [{ name: 'node', type: 'int', multiple: true }], defaultParams: [3], }); addFuncDef({ name: 'maxSeries', shortName: 'max', - category: categories.Combine, + category: 'Combine', }); addFuncDef({ name: 'minSeries', shortName: 'min', - category: categories.Combine, + category: 'Combine', }); addFuncDef({ name: 'averageSeriesWithWildcards', - category: categories.Combine, - params: [{ name: 'node', type: 'int' }, { name: 'node', type: 'int', optional: true }], + category: 'Combine', + params: [{ name: 'node', type: 'int', multiple: true }], defaultParams: [3], }); addFuncDef({ name: 'alias', - category: categories.Special, + category: 'Alias', params: [{ name: 'alias', type: 'string' }], defaultParams: ['alias'], }); addFuncDef({ name: 'aliasSub', - category: categories.Special, + category: 'Alias', params: [{ name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: ['', '\\1'], }); -addFuncDef({ - name: 'stacked', - category: categories.Special, - params: [{ name: 'stack', type: 'string' }], - defaultParams: ['stacked'], -}); - addFuncDef({ name: 'consolidateBy', - category: categories.Special, + category: 'Special', params: [ { name: 'function', @@ -207,14 +180,14 @@ addFuncDef({ addFuncDef({ name: 'cumulative', - category: categories.Special, + category: 'Special', params: [], defaultParams: [], }); addFuncDef({ name: 'groupByNode', - category: categories.Special, + category: 'Combine', params: [ { name: 'node', @@ -232,30 +205,13 @@ addFuncDef({ addFuncDef({ name: 'aliasByNode', - category: categories.Special, + category: 'Alias', params: [ { name: 'node', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, + multiple: true, }, ], defaultParams: [3], @@ -263,7 +219,7 @@ addFuncDef({ addFuncDef({ name: 'substr', - category: categories.Special, + category: 'Special', params: [ { name: 'start', @@ -281,7 +237,7 @@ addFuncDef({ addFuncDef({ name: 'sortByName', - category: categories.Special, + category: 'Sorting', params: [ { name: 'natural', @@ -295,104 +251,104 @@ addFuncDef({ addFuncDef({ name: 'sortByMaxima', - category: categories.Special, + category: 'Sorting', }); addFuncDef({ name: 'sortByMinima', - category: categories.Special, + category: 'Sorting', }); addFuncDef({ name: 'sortByTotal', - category: categories.Special, + category: 'Sorting', }); addFuncDef({ name: 'aliasByMetric', - category: categories.Special, + category: 'Alias', }); addFuncDef({ name: 'randomWalk', fake: true, - category: categories.Special, + category: 'Special', params: [{ name: 'name', type: 'string' }], defaultParams: ['randomWalk'], }); addFuncDef({ name: 'countSeries', - category: categories.Special, + category: 'Combine', }); addFuncDef({ name: 'constantLine', - category: categories.Special, + category: 'Special', params: [{ name: 'value', type: 'int' }], defaultParams: [10], }); addFuncDef({ name: 'cactiStyle', - category: categories.Special, + category: 'Special', }); addFuncDef({ name: 'keepLastValue', - category: categories.Special, + category: 'Transform', params: [{ name: 'n', type: 'int' }], defaultParams: [100], }); addFuncDef({ name: 'changed', - category: categories.Special, + category: 'Special', params: [], defaultParams: [], }); addFuncDef({ name: 'scale', - category: categories.Transform, + category: 'Transform', params: [{ name: 'factor', type: 'int' }], defaultParams: [1], }); addFuncDef({ name: 'offset', - category: categories.Transform, + category: 'Transform', params: [{ name: 'amount', type: 'int' }], defaultParams: [10], }); addFuncDef({ name: 'transformNull', - category: categories.Transform, + category: 'Transform', params: [{ name: 'amount', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'integral', - category: categories.Transform, + category: 'Transform', }); addFuncDef({ name: 'derivative', - category: categories.Transform, + category: 'Transform', }); addFuncDef({ name: 'nonNegativeDerivative', - category: categories.Transform, + category: 'Transform', params: [{ name: 'max value or 0', type: 'int', optional: true }], defaultParams: [''], }); addFuncDef({ name: 'timeShift', - category: categories.Transform, + category: 'Transform', params: [ { name: 'amount', @@ -405,7 +361,7 @@ addFuncDef({ addFuncDef({ name: 'timeStack', - category: categories.Transform, + category: 'Transform', params: [ { name: 'timeShiftUnit', @@ -420,7 +376,7 @@ addFuncDef({ addFuncDef({ name: 'summarize', - category: categories.Transform, + category: 'Transform', params: [ { name: 'interval', type: 'string' }, { @@ -440,7 +396,7 @@ addFuncDef({ addFuncDef({ name: 'smartSummarize', - category: categories.Transform, + category: 'Transform', params: [ { name: 'interval', type: 'string' }, { @@ -454,124 +410,124 @@ addFuncDef({ addFuncDef({ name: 'absolute', - category: categories.Transform, + category: 'Transform', }); addFuncDef({ name: 'hitcount', - category: categories.Transform, + category: 'Transform', params: [{ name: 'interval', type: 'string' }], defaultParams: ['10s'], }); addFuncDef({ name: 'log', - category: categories.Transform, + category: 'Transform', params: [{ name: 'base', type: 'int' }], defaultParams: ['10'], }); addFuncDef({ name: 'averageAbove', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'averageBelow', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentAbove', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentBelow', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'maximumAbove', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'maximumBelow', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumAbove', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumBelow', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'limit', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'mostDeviant', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [10], }); addFuncDef({ name: 'exclude', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'exclude', type: 'string' }], defaultParams: ['exclude'], }); addFuncDef({ name: 'highestCurrent', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'highestMax', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestCurrent', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'movingAverage', - category: categories.Filter, + category: 'Calculate', params: [ { name: 'windowSize', @@ -584,7 +540,7 @@ addFuncDef({ addFuncDef({ name: 'movingMedian', - category: categories.Filter, + category: 'Calculate', params: [ { name: 'windowSize', @@ -597,56 +553,56 @@ addFuncDef({ addFuncDef({ name: 'stdev', - category: categories.Filter, + category: 'Calculate', params: [{ name: 'n', type: 'int' }, { name: 'tolerance', type: 'int' }], defaultParams: [5, 0.1], }); addFuncDef({ name: 'highestAverage', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestAverage', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAbovePercentile', - category: categories.Filter, + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAboveValue', - category: categories.Filter, + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowPercentile', - category: categories.Filter, + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowValue', - category: categories.Filter, + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'useSeriesAbove', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'value', type: 'int' }, { name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: [0, 'search', 'replace'], }); @@ -657,7 +613,7 @@ addFuncDef({ addFuncDef({ name: 'aggregateLine', - category: categories.Combine, + category: 'Calculate', params: [ { name: 'func', @@ -671,7 +627,7 @@ addFuncDef({ addFuncDef({ name: 'averageOutsidePercentile', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -679,7 +635,7 @@ addFuncDef({ addFuncDef({ name: 'delay', - category: categories.Transform, + category: 'Transform', params: [{ name: 'steps', type: 'int' }], defaultParams: [1], version: '1.0', @@ -687,7 +643,7 @@ addFuncDef({ addFuncDef({ name: 'exponentialMovingAverage', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -701,7 +657,7 @@ addFuncDef({ addFuncDef({ name: 'fallbackSeries', - category: categories.Special, + category: 'Special', params: [{ name: 'fallback', type: 'string' }], defaultParams: ['constantLine(0)'], version: '1.0', @@ -709,7 +665,7 @@ addFuncDef({ addFuncDef({ name: 'grep', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'grep', type: 'string' }], defaultParams: ['grep'], version: '1.0', @@ -717,7 +673,7 @@ addFuncDef({ addFuncDef({ name: 'groupByNodes', - category: categories.Special, + category: 'Combine', params: [ { name: 'function', @@ -728,24 +684,7 @@ addFuncDef({ name: 'node', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'node', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, + multiple: true, }, ], defaultParams: ['sum', 3], @@ -754,7 +693,7 @@ addFuncDef({ addFuncDef({ name: 'integralByInterval', - category: categories.Transform, + category: 'Transform', params: [ { name: 'intervalUnit', @@ -768,7 +707,7 @@ addFuncDef({ addFuncDef({ name: 'interpolate', - category: categories.Transform, + category: 'Transform', params: [{ name: 'limit', type: 'int', optional: true }], defaultParams: [], version: '1.0', @@ -776,19 +715,19 @@ addFuncDef({ addFuncDef({ name: 'invert', - category: categories.Transform, + category: 'Transform', version: '1.0', }); addFuncDef({ name: 'isNonNull', - category: categories.Combine, + category: 'Combine', version: '1.0', }); addFuncDef({ name: 'linearRegression', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'startSourceAt', @@ -812,13 +751,13 @@ addFuncDef({ shortName: 'map', params: [{ name: 'node', type: 'int' }], defaultParams: [3], - category: categories.Combine, + category: 'Combine', version: '1.0', }); addFuncDef({ name: 'movingMin', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -832,7 +771,7 @@ addFuncDef({ addFuncDef({ name: 'movingMax', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -846,7 +785,7 @@ addFuncDef({ addFuncDef({ name: 'movingSum', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -860,30 +799,13 @@ addFuncDef({ addFuncDef({ name: 'multiplySeriesWithWildcards', - category: categories.Calculate, + category: 'Combine', params: [ { name: 'position', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], - }, - { - name: 'position', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'position', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, - }, - { - name: 'position', - type: 'int', - options: [0, -1, -2, -3, -4, -5, -6, -7], - optional: true, + multiple: true, }, ], defaultParams: [2], @@ -892,13 +814,13 @@ addFuncDef({ addFuncDef({ name: 'offsetToZero', - category: categories.Transform, + category: 'Transform', version: '1.0', }); addFuncDef({ name: 'pow', - category: categories.Transform, + category: 'Transform', params: [{ name: 'factor', type: 'int' }], defaultParams: [10], version: '1.0', @@ -906,7 +828,7 @@ addFuncDef({ addFuncDef({ name: 'powSeries', - category: categories.Transform, + category: 'Transform', params: optionalSeriesRefArgs, defaultParams: [''], version: '1.0', @@ -926,17 +848,16 @@ addFuncDef({ type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], }, - { name: 'reduceMatchers', type: 'string' }, - { name: 'reduceMatchers', type: 'string' }, + { name: 'reduceMatchers', type: 'string', multiple: true }, ], - defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'], - category: categories.Combine, + defaultParams: ['asPercent', 2, 'used_bytes'], + category: 'Combine', version: '1.0', }); addFuncDef({ name: 'removeBetweenPercentile', - category: categories.Filter, + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -944,19 +865,19 @@ addFuncDef({ addFuncDef({ name: 'removeEmptySeries', - category: categories.Filter, + category: 'Filter Series', version: '1.0', }); addFuncDef({ name: 'squareRoot', - category: categories.Transform, + category: 'Transform', version: '1.0', }); addFuncDef({ name: 'timeSlice', - category: categories.Transform, + category: 'Transform', params: [ { name: 'startSliceAt', @@ -976,7 +897,7 @@ addFuncDef({ addFuncDef({ name: 'weightedAverage', - category: categories.Filter, + category: 'Combine', params: [ { name: 'other', type: 'value_or_series', optional: true }, { @@ -991,29 +912,21 @@ addFuncDef({ addFuncDef({ name: 'seriesByTag', - category: categories.Special, - params: [ - { name: 'tagExpression', type: 'string' }, - { name: 'tagExpression', type: 'string', optional: true }, - { name: 'tagExpression', type: 'string', optional: true }, - { name: 'tagExpression', type: 'string', optional: true }, - ], + category: 'Special', + params: [{ name: 'tagExpression', type: 'string', multiple: true }], version: '1.1', }); addFuncDef({ name: 'groupByTags', - category: categories.Special, + category: 'Combine', params: [ { name: 'function', type: 'string', options: ['sum', 'avg', 'maxSeries'], }, - { name: 'tag', type: 'string' }, - { name: 'tag', type: 'string', optional: true }, - { name: 'tag', type: 'string', optional: true }, - { name: 'tag', type: 'string', optional: true }, + { name: 'tag', type: 'string', multiple: true }, ], defaultParams: ['sum', 'tag'], version: '1.1', @@ -1021,23 +934,14 @@ addFuncDef({ addFuncDef({ name: 'aliasByTags', - category: categories.Special, - params: [ - { name: 'tag', type: 'string' }, - { name: 'tag', type: 'string', optional: true }, - { name: 'tag', type: 'string', optional: true }, - { name: 'tag', type: 'string', optional: true }, - ], + category: 'Alias', + params: [{ name: 'tag', type: 'string', multiple: true }], defaultParams: ['tag'], version: '1.1', }); -_.each(categories, function(funcList, catName) { - categories[catName] = _.sortBy(funcList, 'name'); -}); - -function isVersionRelatedFunction(func, graphiteVersion) { - return isVersionGtOrEq(graphiteVersion, func.version) || !func.version; +function isVersionRelatedFunction(obj, graphiteVersion) { + return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version); } export class FuncInstance { @@ -1059,20 +963,34 @@ export class FuncInstance { render(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)) { + var paramType; + if (index < this.def.params.length) { + paramType = this.def.params[index].type; + } else if (_.get(_.last(this.def.params), 'multiple')) { + paramType = _.get(_.last(this.def.params), 'type'); + } + if (paramType === 'value_or_series') { return value; } - + if (paramType === 'boolean' && _.includes(['true', 'false'], value)) { + return value; + } + if (_.includes(['int', 'float', 'int_or_interval', 'node_or_tag', 'node'], paramType) && _.isFinite(+value)) { + return _.toString(+value); + } return "'" + value + "'"; }.bind(this) ); + // don't send any blank parameters to graphite + while (parameters[parameters.length - 1] === '') { + parameters.pop(); + } + if (metricExp) { parameters.unshift(metricExp); } @@ -1085,7 +1003,15 @@ export class FuncInstance { return false; } - return this.def.params[index + 1] && this.def.params[index + 1].optional; + if (this.def.params[index + 1] && this.def.params[index + 1].optional) { + return true; + } + + if (index + 1 >= this.def.params.length && _.get(_.last(this.def.params), 'multiple')) { + return true; + } + + return false; } updateParam(strValue, index) { @@ -1101,7 +1027,7 @@ export class FuncInstance { return; } - if (strValue === '' && this.def.params[index].optional) { + if (strValue === '' && (index >= this.def.params.length || this.def.params[index].optional)) { this.params.splice(index, 1); } else { this.params[index] = strValue; @@ -1123,32 +1049,131 @@ export class FuncInstance { } } -export default { - createFuncInstance: function(funcDef, options?) { - if (_.isString(funcDef)) { - if (!index[funcDef]) { - throw { message: 'Method not found ' + name }; - } - funcDef = index[funcDef]; - } - return new FuncInstance(funcDef, options); - }, +function createFuncInstance(funcDef, options?, idx?) { + if (_.isString(funcDef)) { + funcDef = getFuncDef(funcDef, idx); + } + return new FuncInstance(funcDef, options); +} - getFuncDef: function(name) { - return index[name]; - }, +function getFuncDef(name, idx?) { + if (!(idx || index)[name]) { + throw { message: 'Method not found ' + name }; + } + return (idx || index)[name]; +} - getCategories: function(graphiteVersion) { - var filteredCategories: any = {}; - _.each(categories, function(functions, category) { - var filteredFuncs = _.filter(functions, function(func) { - return isVersionRelatedFunction(func, graphiteVersion); +function getFuncDefs(graphiteVersion, idx?) { + var funcs = {}; + _.forEach(idx || index, function(funcDef) { + if (isVersionRelatedFunction(funcDef, graphiteVersion)) { + funcs[funcDef.name] = _.assign({}, funcDef, { + params: _.filter(funcDef.params, function(param) { + return isVersionRelatedFunction(param, graphiteVersion); + }), }); - if (filteredFuncs.length) { - filteredCategories[category] = filteredFuncs; + } + }); + return funcs; +} + +// parse response from graphite /functions endpoint into internal format +function parseFuncDefs(rawDefs) { + var funcDefs = {}; + + _.forEach(rawDefs || {}, (funcDef, funcName) => { + // skip graphite graph functions + if (funcDef.group === 'Graph') { + return; + } + + var description = funcDef.description; + if (description) { + // tidy up some pydoc syntax that rst2html can't handle + description = description + .replace(/:py:func:`(.+)( <[^>]*>)?`/g, '``$1``') + .replace(/.. seealso:: /g, 'See also: ') + .replace(/.. code-block *:: *none/g, '.. code-block::'); + } + + var func = { + name: funcDef.name, + description: description, + category: funcDef.group, + params: [], + defaultParams: [], + fake: false, + }; + + // get rid of the first "seriesList" param + if (/^seriesLists?$/.test(_.get(funcDef, 'params[0].type', ''))) { + // handle functions that accept multiple seriesLists + // we leave the param in place but mark it optional, so users can add more series if they wish + if (funcDef.params[0].multiple) { + funcDef.params[0].required = false; + // otherwise chop off the first param, it'll be handled separately + } else { + funcDef.params.shift(); } + // tag function as fake + } else { + func.fake = true; + } + + _.forEach(funcDef.params, rawParam => { + var param = { + name: rawParam.name, + type: 'string', + optional: !rawParam.required, + multiple: !!rawParam.multiple, + options: undefined, + }; + + if (rawParam.default !== undefined) { + func.defaultParams.push(_.toString(rawParam.default)); + } else if (rawParam.suggestions) { + func.defaultParams.push(_.toString(rawParam.suggestions[0])); + } else { + func.defaultParams.push(''); + } + + if (rawParam.type === 'boolean') { + param.type = 'boolean'; + param.options = ['true', 'false']; + } else if (rawParam.type === 'integer') { + param.type = 'int'; + } else if (rawParam.type === 'float') { + param.type = 'float'; + } else if (rawParam.type === 'node') { + param.type = 'node'; + param.options = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; + } else if (rawParam.type === 'nodeOrTag') { + param.type = 'node_or_tag'; + param.options = ['name', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; + } else if (rawParam.type === 'intOrInterval') { + param.type = 'int_or_interval'; + } else if (rawParam.type === 'seriesList') { + param.type = 'value_or_series'; + } + + if (rawParam.options) { + param.options = _.map(rawParam.options, _.toString); + } else if (rawParam.suggestions) { + param.options = _.map(rawParam.suggestions, _.toString); + } + + func.params.push(param); }); - return filteredCategories; - }, + funcDefs[funcName] = func; + }); + + return funcDefs; +} + +export default { + createFuncInstance: createFuncInstance, + getFuncDef: getFuncDef, + getFuncDefs: getFuncDefs, + parseFuncDefs: parseFuncDefs, }; diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 9f9d4d12701..10e0144e3ff 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import gfunc from './gfunc'; import { Parser } from './parser'; export default class GraphiteQuery { + datasource: any; target: any; functions: any[]; segments: any[]; @@ -15,7 +15,8 @@ export default class GraphiteQuery { scopedVars: any; /** @ngInject */ - constructor(target, templateSrv?, scopedVars?) { + constructor(datasource, target, templateSrv?, scopedVars?) { + this.datasource = datasource; this.target = target; this.parseTarget(); @@ -86,7 +87,7 @@ export default class GraphiteQuery { switch (astNode.type) { case 'function': - var innerFunc = gfunc.createFuncInstance(astNode.name, { + var innerFunc = this.datasource.createFuncInstance(astNode.name, { withDefaultParams: false, }); _.each(astNode.params, param => { @@ -133,7 +134,7 @@ export default class GraphiteQuery { moveAliasFuncLast() { var aliasFunc = _.find(this.functions, function(func) { - return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric'; + return func.def.name.startsWith('alias'); }); if (aliasFunc) { @@ -143,7 +144,7 @@ export default class GraphiteQuery { } addFunctionParameter(func, value) { - if (func.params.length >= func.def.params.length) { + if (func.params.length >= func.def.params.length && !_.get(_.last(func.def.params), 'multiple', false)) { throw { message: 'too many parameters for function ' + func.def.name }; } func.params.push(value); @@ -208,7 +209,7 @@ export default class GraphiteQuery { } splitSeriesByTagParams(func) { - const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/; + const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/; return _.flatten( _.map(func.params, (param: string) => { let matches = tagPattern.exec(param); diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index b79164e1781..6414a39c94f 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -10,30 +10,50 @@
-
- + - - + - - + - + on-change="ctrl.tagChanged(tag, $index)" + />
-