From 3a4e05133e8799a0219f683bc4ca0d9dcb84d2b9 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 8 Dec 2017 18:48:39 -0500 Subject: [PATCH 01/21] support for loading function definitions from graphite --- .editorconfig | 1 - package.json | 3 +- .../datasource/graphite/add_graphite_func.js | 134 +++--- .../plugins/datasource/graphite/datasource.ts | 125 ++++++ .../datasource/graphite/func_editor.js | 81 +++- .../app/plugins/datasource/graphite/gfunc.ts | 421 ++++++++---------- .../datasource/graphite/graphite_query.ts | 11 +- .../graphite/partials/query.editor.html | 2 +- .../plugins/datasource/graphite/query_ctrl.ts | 8 +- .../datasource/graphite/specs/gfunc.jest.ts | 11 +- .../graphite/specs/query_ctrl_specs.ts | 21 +- public/sass/components/_query_editor.scss | 1 + public/sass/components/_query_part.scss | 8 + yarn.lock | 183 +++++++- 14 files changed, 655 insertions(+), 355 deletions(-) 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 01241637d46..13b8b9e3955 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,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", @@ -154,6 +154,7 @@ "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", + "rst2html": "github:thoward/rst2html#d6e2f21", "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index b4df928adca..10d9b9ced71 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -2,13 +2,10 @@ define([ 'angular', 'lodash', 'jquery', - './gfunc', ], -function (angular, _, $, gfunc) { +function (angular, _, $) { 'use strict'; - gfunc = gfunc.default; - angular .module('grafana.directives') .directive('graphiteAddFunc', function($compile) { @@ -23,91 +20,92 @@ function (angular, _, $, gfunc) { return { link: function($scope, elem) { var ctrl = $scope.ctrl; - var graphiteVersion = ctrl.datasource.graphiteVersion; - var categories = gfunc.getCategories(graphiteVersion); - 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 = gfunc.getFuncDef(value); - if (!funcDef) { - // try find close match - value = value.toLowerCase(); - funcDef = _.find(allFunctions, function(funcName) { - return funcName.toLowerCase().indexOf(value) === 0; + 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 = ctrl.datasource.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() { + ctrl.addFunction(funcDef); }); - if (!funcDef) { return; } + $input.trigger('blur'); + return ''; } + }); - $scope.$apply(function() { - ctrl.addFunction(funcDef); - }); + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); - $input.trigger('blur'); - return ''; - } + $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); }); - - $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(funcDefs) { + var categories = {}; - function createFunctionDropDownMenu(categories) { - return _.map(categories, function(list, category) { - var submenu = _.map(list, function(value) { - return { - text: value.name, - click: "ctrl.addFunction('" + value.name + "')", - }; + _.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: submenu + submenu: _.sortBy(submenu, 'text') }; - }); + }), 'text'); } }); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 38133f801d8..8733a167da4 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,7 @@ 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.getQueryOptionsInfo = function() { return { @@ -347,6 +349,125 @@ 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.getFuncDefs = function() { + let self = this; + + if (self.funcDefs !== null) { + return Promise.resolve(self.funcDefs); + } + + if (!supportsFunctionIndex(self.graphiteVersion)) { + self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); + return Promise.resolve(self.funcDefs); + } + + let httpOptions = { + method: 'GET', + url: '/functions', + }; + + return self + .doGraphiteRequest(httpOptions) + .then(results => { + if (results.status !== 200 || typeof results.data !== 'object') { + self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); + return Promise.resolve(self.funcDefs); + } + + self.funcDefs = {}; + _.forEach(results.data || {}, (funcDef, funcName) => { + // skip graphite graph functions + if (funcDef.group === 'Graph') { + return; + } + + var func = { + name: funcDef.name, + description: funcDef.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); + }); + + self.funcDefs[funcName] = func; + }); + return self.funcDefs; + }) + .catch(err => { + self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); + return self.funcDefs; + }); + }; + this.testDatasource = function() { return this.metricFindQuery('*').then(function() { return { status: 'success', message: 'Data source is working' }; @@ -440,3 +561,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..08c11d6c5b7 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -2,8 +2,9 @@ define([ 'angular', 'lodash', 'jquery', + 'rst2html', ], -function (angular, _, $) { +function (angular, _, $, rst2html) { 'use strict'; angular @@ -12,7 +13,7 @@ function (angular, _, $) { var funcSpanTemplate = '{{func.def.name}}('; var paramTemplate = ''; + ' class="input-small tight-form-func-param">'; var funcControlsTemplate = '
' + @@ -29,7 +30,6 @@ function (angular, _, $) { var $funcControls = $(funcControlsTemplate); var ctrl = $scope.ctrl; var func = $scope.func; - var funcDef = func.def; var scheduledRelink = false; var paramCountAtLink = 0; @@ -37,11 +37,12 @@ function (angular, _, $) { /*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,22 +69,42 @@ function (angular, _, $) { } } + 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 inputBlur(paramIndex) { /*jshint validthis:true */ var $input = $(this); + if ($input.data('typeahead') && $input.data('typeahead').shown) { + return; + } + var $link = $input.prev(); + var $comma = $link.prev('.comma'); var newValue = $input.val(); - if (newValue !== '' || func.def.params[paramIndex].optional) { + if (newValue !== '' || paramDef(paramIndex).optional) { $link.html(templateSrv.highlightVariablesAsHtml(newValue)); - func.updateParam($input.val(), paramIndex); + func.updateParam(newValue, paramIndex); scheduledRelinkIfNeeded(); $scope.$apply(function() { ctrl.targetChanged(); }); + if ($link.hasClass('last') && newValue === '') { + $comma.addClass('last'); + } else { + $link.removeClass('last'); + } $input.hide(); $link.show(); } @@ -104,8 +125,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(); }); } @@ -148,18 +169,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 +208,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 +218,7 @@ function (angular, _, $) { $compile(elem.contents())($scope); } - function ifJustAddedFocusFistParam() { + function ifJustAddedFocusFirstParam() { if ($scope.func.added) { $scope.func.added = false; setTimeout(function() { @@ -223,7 +259,12 @@ function (angular, _, $) { } if ($target.hasClass('fa-question-circle')) { - window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank'); + if (func.def.description) { + alert(rst2html(func.def.description)); + } else { + window.open( + "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank'); + } return; } }); @@ -233,7 +274,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..fdfa67be1f9 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: 'Calculate', }); addFuncDef({ name: 'stddevSeries', params: optionalSeriesRefArgs, defaultParams: [''], - category: categories.Calculate, + category: 'Calculate', }); addFuncDef({ name: 'divideSeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Calculate', }); addFuncDef({ name: 'multiplySeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Calculate', }); addFuncDef({ name: 'asPercent', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: categories.Calculate, + category: 'Calculate', }); 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,73 @@ 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: 'Special', params: [{ name: 'alias', type: 'string' }], defaultParams: ['alias'], }); addFuncDef({ name: 'aliasSub', - category: categories.Special, + category: 'Special', params: [{ name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: ['', '\\1'], }); addFuncDef({ name: 'stacked', - category: categories.Special, + category: 'Special', params: [{ name: 'stack', type: 'string' }], defaultParams: ['stacked'], }); addFuncDef({ name: 'consolidateBy', - category: categories.Special, + category: 'Special', params: [ { name: 'function', @@ -207,14 +187,14 @@ addFuncDef({ addFuncDef({ name: 'cumulative', - category: categories.Special, + category: 'Special', params: [], defaultParams: [], }); addFuncDef({ name: 'groupByNode', - category: categories.Special, + category: 'Special', params: [ { name: 'node', @@ -232,30 +212,13 @@ addFuncDef({ addFuncDef({ name: 'aliasByNode', - category: categories.Special, + category: 'Special', 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 +226,7 @@ addFuncDef({ addFuncDef({ name: 'substr', - category: categories.Special, + category: 'Special', params: [ { name: 'start', @@ -281,7 +244,7 @@ addFuncDef({ addFuncDef({ name: 'sortByName', - category: categories.Special, + category: 'Special', params: [ { name: 'natural', @@ -295,104 +258,104 @@ addFuncDef({ addFuncDef({ name: 'sortByMaxima', - category: categories.Special, + category: 'Special', }); addFuncDef({ name: 'sortByMinima', - category: categories.Special, + category: 'Special', }); addFuncDef({ name: 'sortByTotal', - category: categories.Special, + category: 'Special', }); addFuncDef({ name: 'aliasByMetric', - category: categories.Special, + category: 'Special', }); addFuncDef({ name: 'randomWalk', fake: true, - category: categories.Special, + category: 'Special', params: [{ name: 'name', type: 'string' }], defaultParams: ['randomWalk'], }); addFuncDef({ name: 'countSeries', - category: categories.Special, + category: 'Special', }); 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: 'Special', 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 +368,7 @@ addFuncDef({ addFuncDef({ name: 'timeStack', - category: categories.Transform, + category: 'Transform', params: [ { name: 'timeShiftUnit', @@ -420,7 +383,7 @@ addFuncDef({ addFuncDef({ name: 'summarize', - category: categories.Transform, + category: 'Transform', params: [ { name: 'interval', type: 'string' }, { @@ -440,7 +403,7 @@ addFuncDef({ addFuncDef({ name: 'smartSummarize', - category: categories.Transform, + category: 'Transform', params: [ { name: 'interval', type: 'string' }, { @@ -454,124 +417,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', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'averageBelow', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentAbove', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentBelow', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'maximumAbove', - category: categories.Filter, + category: 'Filter', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'maximumBelow', - category: categories.Filter, + category: 'Filter', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumAbove', - category: categories.Filter, + category: 'Filter', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumBelow', - category: categories.Filter, + category: 'Filter', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'limit', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'mostDeviant', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [10], }); addFuncDef({ name: 'exclude', - category: categories.Filter, + category: 'Filter', params: [{ name: 'exclude', type: 'string' }], defaultParams: ['exclude'], }); addFuncDef({ name: 'highestCurrent', - category: categories.Filter, + category: 'Filter', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'highestMax', - category: categories.Filter, + category: 'Filter', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestCurrent', - category: categories.Filter, + category: 'Filter', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'movingAverage', - category: categories.Filter, + category: 'Filter', params: [ { name: 'windowSize', @@ -584,7 +547,7 @@ addFuncDef({ addFuncDef({ name: 'movingMedian', - category: categories.Filter, + category: 'Filter', params: [ { name: 'windowSize', @@ -597,56 +560,56 @@ addFuncDef({ addFuncDef({ name: 'stdev', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }, { name: 'tolerance', type: 'int' }], defaultParams: [5, 0.1], }); addFuncDef({ name: 'highestAverage', - category: categories.Filter, + category: 'Filter', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestAverage', - category: categories.Filter, + category: 'Filter', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAbovePercentile', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAboveValue', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowPercentile', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowValue', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'useSeriesAbove', - category: categories.Filter, + category: 'Filter', params: [{ name: 'value', type: 'int' }, { name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: [0, 'search', 'replace'], }); @@ -657,7 +620,7 @@ addFuncDef({ addFuncDef({ name: 'aggregateLine', - category: categories.Combine, + category: 'Combine', params: [ { name: 'func', @@ -671,7 +634,7 @@ addFuncDef({ addFuncDef({ name: 'averageOutsidePercentile', - category: categories.Filter, + category: 'Filter', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -679,7 +642,7 @@ addFuncDef({ addFuncDef({ name: 'delay', - category: categories.Transform, + category: 'Transform', params: [{ name: 'steps', type: 'int' }], defaultParams: [1], version: '1.0', @@ -687,7 +650,7 @@ addFuncDef({ addFuncDef({ name: 'exponentialMovingAverage', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -701,7 +664,7 @@ addFuncDef({ addFuncDef({ name: 'fallbackSeries', - category: categories.Special, + category: 'Special', params: [{ name: 'fallback', type: 'string' }], defaultParams: ['constantLine(0)'], version: '1.0', @@ -709,7 +672,7 @@ addFuncDef({ addFuncDef({ name: 'grep', - category: categories.Filter, + category: 'Filter', params: [{ name: 'grep', type: 'string' }], defaultParams: ['grep'], version: '1.0', @@ -717,7 +680,7 @@ addFuncDef({ addFuncDef({ name: 'groupByNodes', - category: categories.Special, + category: 'Special', params: [ { name: 'function', @@ -728,24 +691,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 +700,7 @@ addFuncDef({ addFuncDef({ name: 'integralByInterval', - category: categories.Transform, + category: 'Transform', params: [ { name: 'intervalUnit', @@ -768,7 +714,7 @@ addFuncDef({ addFuncDef({ name: 'interpolate', - category: categories.Transform, + category: 'Transform', params: [{ name: 'limit', type: 'int', optional: true }], defaultParams: [], version: '1.0', @@ -776,19 +722,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 +758,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 +778,7 @@ addFuncDef({ addFuncDef({ name: 'movingMax', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -846,7 +792,7 @@ addFuncDef({ addFuncDef({ name: 'movingSum', - category: categories.Calculate, + category: 'Calculate', params: [ { name: 'windowSize', @@ -860,30 +806,13 @@ addFuncDef({ addFuncDef({ name: 'multiplySeriesWithWildcards', - category: categories.Calculate, + category: 'Calculate', 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 +821,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 +835,7 @@ addFuncDef({ addFuncDef({ name: 'powSeries', - category: categories.Transform, + category: 'Transform', params: optionalSeriesRefArgs, defaultParams: [''], version: '1.0', @@ -926,17 +855,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', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -944,19 +872,19 @@ addFuncDef({ addFuncDef({ name: 'removeEmptySeries', - category: categories.Filter, + category: 'Filter', 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 +904,7 @@ addFuncDef({ addFuncDef({ name: 'weightedAverage', - category: categories.Filter, + category: 'Filter', params: [ { name: 'other', type: 'value_or_series', optional: true }, { @@ -991,29 +919,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: 'Special', 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 +941,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: 'Special', + 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 +970,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 +1010,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 +1034,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 +1056,36 @@ 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; +} - return filteredCategories; - }, +export default { + createFuncInstance: createFuncInstance, + getFuncDef: getFuncDef, + getFuncDefs: getFuncDefs, }; diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 9f9d4d12701..9f651e72744 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); diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index b79164e1781..c1889cb92fa 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -18,7 +18,7 @@ + min-input-width="30"> Date: Fri, 22 Dec 2017 15:08:59 -0500 Subject: [PATCH 02/21] work on tag dropdown behavior --- .../components/form_dropdown/form_dropdown.ts | 37 ++++++++++++----- .../app/core/directives/dropdown_typeahead.js | 2 +- public/app/core/services/segment_srv.js | 4 -- .../datasource/graphite/add_graphite_func.js | 2 +- .../datasource/graphite/graphite_query.ts | 2 +- .../graphite/partials/query.editor.html | 40 +++++++++++++------ .../plugins/datasource/graphite/query_ctrl.ts | 4 +- .../graphite/specs/query_ctrl_specs.ts | 4 +- 8 files changed, 61 insertions(+), 34 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 1fa1dea4338..f3d920c5b4d 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); } } @@ -165,7 +176,7 @@ export class FormDropdownCtrl { updateValue(text) { text = _.unescape(text); - if (text === '' || this.text === text) { + if ((!this.allowCustom && text === '') || this.text === text) { return; } @@ -214,7 +225,9 @@ export class FormDropdownCtrl { var typeahead = this.inputElement.data('typeahead'); if (typeahead) { - this.inputElement.val(''); + if (!this.allowCustom) { + this.inputElement.val(''); + } typeahead.lookup(); } } @@ -228,10 +241,10 @@ const template = ` style="display:none"> + tabindex="1" + ng-click="ctrl.open()" + give-focus="ctrl.focus" + ng-bind-html="ctrl.display || ' '"> `; @@ -250,6 +263,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/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 10d9b9ced71..97c34664ed7 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -13,7 +13,7 @@ function (angular, _, $) { ' class="gf-form-input"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = '' + ''; diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 9f651e72744..10e0144e3ff 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -209,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 c1889cb92fa..df32353644d 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -11,29 +11,45 @@
- - - + - - + - + on-change="ctrl.tagChanged(tag, $index)" + start-open="!ctrl.showDelimiter($index)" + />
diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 72b72cfbb32..d8cc6c4c16b 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -270,7 +270,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { let newFunc = this.datasource.createFuncInstance('seriesByTag', { withDefaultParams: false, }); - let tagParam = `${tag}=select tag value`; + let tagParam = `${tag}=`; newFunc.params = [tagParam]; this.queryModel.addFunction(newFunc); newFunc.added = true; @@ -353,7 +353,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { addNewTag(segment) { let newTagKey = segment.value; - let newTag = { key: newTagKey, operator: '=', value: 'select tag value' }; + let newTag = { key: newTagKey, operator: '=', value: '' }; this.queryModel.addTag(newTag); this.targetChanged(); this.fixTagSegments(); diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts index 70fc5bbb7b6..fbbd9134c3e 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts @@ -284,12 +284,12 @@ describe('GraphiteQueryCtrl', function() { }); it('should update tags with default value', function() { - const expected = [{ key: 'tag1', operator: '=', value: 'select tag value' }]; + const expected = [{ key: 'tag1', operator: '=', value: '' }]; expect(ctx.ctrl.queryModel.tags).to.eql(expected); }); it('should update target', function() { - const expected = "seriesByTag('tag1=select tag value')"; + const expected = "seriesByTag('tag1=')"; expect(ctx.ctrl.target.target).to.eql(expected); }); }); From a3dba6070efb8b04c6681695c065c4d82340528c Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 22 Dec 2017 15:32:19 -0500 Subject: [PATCH 03/21] sync function categories with graphite-web --- .../app/plugins/datasource/graphite/gfunc.ts | 107 ++++++++---------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index fdfa67be1f9..8207a70b868 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -59,35 +59,35 @@ addFuncDef({ name: 'diffSeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: 'Calculate', + category: 'Combine', }); addFuncDef({ name: 'stddevSeries', params: optionalSeriesRefArgs, defaultParams: [''], - category: 'Calculate', + category: 'Combine', }); addFuncDef({ name: 'divideSeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: 'Calculate', + category: 'Combine', }); addFuncDef({ name: 'multiplySeries', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: 'Calculate', + category: 'Combine', }); addFuncDef({ name: 'asPercent', params: optionalSeriesRefArgs, defaultParams: ['#A'], - category: 'Calculate', + category: 'Combine', }); addFuncDef({ @@ -153,25 +153,18 @@ addFuncDef({ addFuncDef({ name: 'alias', - category: 'Special', + category: 'Alias', params: [{ name: 'alias', type: 'string' }], defaultParams: ['alias'], }); addFuncDef({ name: 'aliasSub', - category: 'Special', + category: 'Alias', params: [{ name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: ['', '\\1'], }); -addFuncDef({ - name: 'stacked', - category: 'Special', - params: [{ name: 'stack', type: 'string' }], - defaultParams: ['stacked'], -}); - addFuncDef({ name: 'consolidateBy', category: 'Special', @@ -194,7 +187,7 @@ addFuncDef({ addFuncDef({ name: 'groupByNode', - category: 'Special', + category: 'Combine', params: [ { name: 'node', @@ -212,7 +205,7 @@ addFuncDef({ addFuncDef({ name: 'aliasByNode', - category: 'Special', + category: 'Alias', params: [ { name: 'node', @@ -244,7 +237,7 @@ addFuncDef({ addFuncDef({ name: 'sortByName', - category: 'Special', + category: 'Sorting', params: [ { name: 'natural', @@ -258,22 +251,22 @@ addFuncDef({ addFuncDef({ name: 'sortByMaxima', - category: 'Special', + category: 'Sorting', }); addFuncDef({ name: 'sortByMinima', - category: 'Special', + category: 'Sorting', }); addFuncDef({ name: 'sortByTotal', - category: 'Special', + category: 'Sorting', }); addFuncDef({ name: 'aliasByMetric', - category: 'Special', + category: 'Alias', }); addFuncDef({ @@ -286,7 +279,7 @@ addFuncDef({ addFuncDef({ name: 'countSeries', - category: 'Special', + category: 'Combine', }); addFuncDef({ @@ -303,7 +296,7 @@ addFuncDef({ addFuncDef({ name: 'keepLastValue', - category: 'Special', + category: 'Transform', params: [{ name: 'n', type: 'int' }], defaultParams: [100], }); @@ -436,105 +429,105 @@ addFuncDef({ addFuncDef({ name: 'averageAbove', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'averageBelow', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentAbove', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'currentBelow', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [25], }); addFuncDef({ name: 'maximumAbove', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'maximumBelow', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumAbove', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'minimumBelow', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'value', type: 'int' }], defaultParams: [0], }); addFuncDef({ name: 'limit', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'mostDeviant', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [10], }); addFuncDef({ name: 'exclude', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'exclude', type: 'string' }], defaultParams: ['exclude'], }); addFuncDef({ name: 'highestCurrent', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'highestMax', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestCurrent', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'movingAverage', - category: 'Filter', + category: 'Calculate', params: [ { name: 'windowSize', @@ -547,7 +540,7 @@ addFuncDef({ addFuncDef({ name: 'movingMedian', - category: 'Filter', + category: 'Calculate', params: [ { name: 'windowSize', @@ -560,56 +553,56 @@ addFuncDef({ addFuncDef({ name: 'stdev', - category: 'Filter', + category: 'Calculate', params: [{ name: 'n', type: 'int' }, { name: 'tolerance', type: 'int' }], defaultParams: [5, 0.1], }); addFuncDef({ name: 'highestAverage', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'lowestAverage', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'count', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAbovePercentile', - category: 'Filter', + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeAboveValue', - category: 'Filter', + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowPercentile', - category: 'Filter', + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'removeBelowValue', - category: 'Filter', + category: 'Filter Data', params: [{ name: 'n', type: 'int' }], defaultParams: [5], }); addFuncDef({ name: 'useSeriesAbove', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'value', type: 'int' }, { name: 'search', type: 'string' }, { name: 'replace', type: 'string' }], defaultParams: [0, 'search', 'replace'], }); @@ -620,7 +613,7 @@ addFuncDef({ addFuncDef({ name: 'aggregateLine', - category: 'Combine', + category: 'Calculate', params: [ { name: 'func', @@ -634,7 +627,7 @@ addFuncDef({ addFuncDef({ name: 'averageOutsidePercentile', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -672,7 +665,7 @@ addFuncDef({ addFuncDef({ name: 'grep', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'grep', type: 'string' }], defaultParams: ['grep'], version: '1.0', @@ -680,7 +673,7 @@ addFuncDef({ addFuncDef({ name: 'groupByNodes', - category: 'Special', + category: 'Combine', params: [ { name: 'function', @@ -806,7 +799,7 @@ addFuncDef({ addFuncDef({ name: 'multiplySeriesWithWildcards', - category: 'Calculate', + category: 'Combine', params: [ { name: 'position', @@ -864,7 +857,7 @@ addFuncDef({ addFuncDef({ name: 'removeBetweenPercentile', - category: 'Filter', + category: 'Filter Series', params: [{ name: 'n', type: 'int' }], defaultParams: [95], version: '1.0', @@ -872,7 +865,7 @@ addFuncDef({ addFuncDef({ name: 'removeEmptySeries', - category: 'Filter', + category: 'Filter Series', version: '1.0', }); @@ -904,7 +897,7 @@ addFuncDef({ addFuncDef({ name: 'weightedAverage', - category: 'Filter', + category: 'Combine', params: [ { name: 'other', type: 'value_or_series', optional: true }, { @@ -926,7 +919,7 @@ addFuncDef({ addFuncDef({ name: 'groupByTags', - category: 'Special', + category: 'Combine', params: [ { name: 'function', @@ -941,7 +934,7 @@ addFuncDef({ addFuncDef({ name: 'aliasByTags', - category: 'Special', + category: 'Alias', params: [{ name: 'tag', type: 'string', multiple: true }], defaultParams: ['tag'], version: '1.1', From d8f2a207750535f3f61f68b33cc738c058465424 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 27 Dec 2017 12:31:44 -0500 Subject: [PATCH 04/21] add button to trigger evaluation of tag queries --- .../graphite/partials/query.editor.html | 19 ++++++++++++++----- .../plugins/datasource/graphite/query_ctrl.ts | 14 +++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index df32353644d..18511ca8d24 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -10,7 +10,7 @@
-
+
AND
- - + + +
+ +
+ +
diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index d8cc6c4c16b..f37cc8e8a6d 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -17,11 +17,13 @@ export class GraphiteQueryCtrl extends QueryCtrl { addTagSegments: any[]; removeTagValue: string; supportsTags: boolean; + paused: boolean; /** @ngInject **/ constructor($scope, $injector, private uiSegmentSrv, private templateSrv) { super($scope, $injector); this.supportsTags = this.datasource.supportsTags; + this.paused = false; if (this.target) { this.target.target = this.target.target || ''; @@ -191,6 +193,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { if (segment.type === 'tag') { let tag = removeTagPrefix(segment.value); + this.pause(); this.addSeriesByTagFunc(tag); return; } @@ -235,7 +238,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { var oldTarget = this.queryModel.target.target; this.updateModelTarget(); - if (this.queryModel.target !== oldTarget) { + if (this.queryModel.target !== oldTarget && !this.paused) { this.panelCtrl.refresh(); } } @@ -372,6 +375,15 @@ export class GraphiteQueryCtrl extends QueryCtrl { showDelimiter(index) { return index !== this.queryModel.tags.length - 1; } + + pause() { + this.paused = true; + } + + unpause() { + this.paused = false; + this.panelCtrl.refresh(); + } } function mapToDropdownOptions(results) { From ff759b0ef74107e819243524474c113f20b6d479 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 27 Dec 2017 13:48:55 -0500 Subject: [PATCH 05/21] send prefix when auto-completing tags --- .../datasource/graphite/partials/query.editor.html | 4 ++-- .../app/plugins/datasource/graphite/query_ctrl.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 18511ca8d24..e267238ef96 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -45,11 +45,11 @@
diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index f37cc8e8a6d..b9ed9f3cb9c 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -105,7 +105,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { }); } - getAltSegments(index) { + getAltSegments(index, prefix) { var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*'; var options = { range: this.panelCtrl.range, @@ -122,7 +122,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { }); }); - if (altSegments.length === 0) { + if (index > 0 && altSegments.length === 0) { return altSegments; } @@ -159,7 +159,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { if (this.supportsTags && index === 0) { this.removeTaggedEntry(altSegments); - return this.addAltTagSegments(index, altSegments); + return this.addAltTagSegments(prefix, altSegments); } else { return altSegments; } @@ -169,8 +169,8 @@ export class GraphiteQueryCtrl extends QueryCtrl { }); } - addAltTagSegments(index, altSegments) { - return this.getTagsAsSegments().then(tagSegments => { + addAltTagSegments(prefix, altSegments) { + return this.getTagsAsSegments(prefix).then(tagSegments => { tagSegments = _.map(tagSegments, segment => { segment.value = TAG_PREFIX + segment.value; return segment; @@ -315,9 +315,9 @@ export class GraphiteQueryCtrl extends QueryCtrl { }); } - getTagsAsSegments() { + getTagsAsSegments(tagPrefix) { let tagExpressions = this.queryModel.renderTagExpressions(); - return this.datasource.getTagsAutoComplete(tagExpressions).then(values => { + return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => { return _.map(values, val => { return this.uiSegmentSrv.newSegment({ value: val.text, From 7c1be021acd6f266725a998008289f58f6e25fe1 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 27 Dec 2017 13:53:12 -0500 Subject: [PATCH 06/21] use typeahead value in graphite find requests --- public/app/plugins/datasource/graphite/query_ctrl.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index b9ed9f3cb9c..d7bb924a4f6 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -106,7 +106,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { } getAltSegments(index, prefix) { - var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*'; + var query = '*' + prefix + '*'; + if (index > 0) { + query = this.queryModel.getSegmentPathUpTo(index) + '.' + query; + } var options = { range: this.panelCtrl.range, requestId: 'get-alt-segments', From 80a6e0d8d13804c51bc73338e5abb29c445d1b5f Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 27 Dec 2017 15:32:15 -0500 Subject: [PATCH 07/21] support specifying tag_values("") as graphite template query --- .../features/templating/partials/editor.html | 8 +-- .../plugins/datasource/graphite/datasource.ts | 71 ++++++++++++++++--- 2 files changed, 67 insertions(+), 12 deletions(-) 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/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 8733a167da4..49b5be1dfbc 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -202,6 +202,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', @@ -212,7 +241,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); } @@ -237,7 +266,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); } @@ -262,7 +291,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); } @@ -281,18 +310,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: expressions, }, + // 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) { @@ -305,19 +345,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; - this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => { + this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, limit, optionalOptions) => { + let options = optionalOptions || {}; + let httpOptions: any = { method: 'GET', url: '/tags/autoComplete/values', params: { - expr: expression, + expr: expressions, tag: 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) { @@ -330,10 +381,14 @@ 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 + // for cancellations + requestId: options.requestId, }; return this.doGraphiteRequest(httpOptions) From 4d3bac028415f5422d6d5255a92e848cb8bf10ff Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 27 Dec 2017 22:29:19 -0500 Subject: [PATCH 08/21] tooltips for function definitions --- .../datasource/graphite/add_graphite_func.js | 52 ++++++++++++++++++- .../datasource/graphite/func_editor.js | 12 +++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 97c34664ed7..c66176aeb8f 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -2,13 +2,15 @@ define([ 'angular', 'lodash', 'jquery', + 'rst2html', + 'tether-drop', ], -function (angular, _, $) { +function (angular, _, $, rst2html, Drop) { 'use strict'; angular .module('grafana.directives') - .directive('graphiteAddFunc', function($compile) { + .directive('graphiteAddFunc', function($compile) { var inputTemplate = ''; @@ -81,6 +83,52 @@ function (angular, _, $) { $compile(elem.contents())($scope); }); + + var drops = []; + + $(elem) + .on('mouseenter', 'ul.dropdown-menu li', function () { + while (drops.length > 0) { + drops.pop().remove(); + } + + 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); + + var drop = new Drop({ + target: this, + content: contentElement, + classes: 'drop-popover', + openOn: 'always', + tetherOptions: { + attachment: 'bottom left', + targetAttachment: 'bottom right', + }, + }); + + drops.push(drop); + + drop.open(); + } + }) + .on('mouseout', 'ul.dropdown-menu li', function() { + while (drops.length > 0) { + drops.pop().remove(); + } + }); } }; }); diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 08c11d6c5b7..764ac0ad22c 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -9,7 +9,7 @@ function (angular, _, $, rst2html) { angular .module('grafana.directives') - .directive('graphiteFuncEditor', function($compile, templateSrv) { + .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) { var funcSpanTemplate = '{{func.def.name}}('; var paramTemplate = '

' + 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'); From 3ddb65fc8bf1511826dbd9958cd17e1ddbd6879b Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 28 Dec 2017 16:14:39 -0500 Subject: [PATCH 09/21] function description formatting --- .../plugins/datasource/graphite/datasource.ts | 11 +++++- .../datasource/graphite/func_editor.js | 3 +- public/sass/components/_query_editor.scss | 35 +++++++++++++++++-- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 49b5be1dfbc..0c8e1ca0843 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -444,9 +444,18 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv 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: funcDef.description, + description: description, category: funcDef.group, params: [], defaultParams: [], diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 764ac0ad22c..2d1d94b82cf 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -264,7 +264,8 @@ function (angular, _, $, rst2html) { popoverSrv.show({ element: e.target, position: 'bottom left', - template: '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', + classNames: 'drop-popover drop-function-def', + template: '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', openOn: 'click', }); } else { diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 23bccf4b2cd..a99bd1bbd79 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -89,7 +89,7 @@ } } -input[type="text"].tight-form-func-param { +input[type='text'].tight-form-func-param { background: transparent; border: none; margin: 0; @@ -129,7 +129,7 @@ input[type="text"].tight-form-func-param { } } -input[type="text"].tight-form-func-param { +input[type='text'].tight-form-func-param { font-size: 0.875rem; background: transparent; border: none; @@ -177,3 +177,34 @@ input[type="text"].tight-form-func-param { .query-troubleshooter__body { padding: $spacer 0; } + +.rst-text::before { + content: ' '; +} + +.rst-unknown.rst-directive { + font-family: monospace; + margin-bottom: 1rem; +} + +.rst-interpreted_text { + font-family: monospace; + display: inline; +} + +.rst-bullet-list { + padding-left: 1.5rem; + margin-bottom: 1rem; +} + +.rst-paragraph:last-child { + margin-bottom: 0; +} + +.drop-element.drop-popover.drop-function-def .drop-content { + max-width: 30rem; +} + +.rst-literal-block .rst-text { + display: block; +} From da9e1a7e9814649133c4be8ca38357bd22d53e3d Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 28 Dec 2017 16:35:13 -0500 Subject: [PATCH 10/21] fix line length, run jscs & jshint in precommit --- public/app/plugins/datasource/graphite/func_editor.js | 3 ++- scripts/grunt/default_task.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 2d1d94b82cf..af4318b0877 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -265,7 +265,8 @@ function (angular, _, $, rst2html) { element: e.target, position: 'bottom left', classNames: 'drop-popover drop-function-def', - template: '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', + template: '
' + + '

' + funcDef.name + '

' + rst2html(funcDef.description) + '
', openOn: 'click', }); } else { diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index a96e4fdb377..e8b2d1a13f7 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -20,6 +20,8 @@ module.exports = function(grunt) { ]); grunt.registerTask('precommit', [ + 'jscs', + 'jshint', 'sasslint', 'exec:tslint', 'no-only-tests' From 1d00c3c72a6097f9a9a4101180a31abc266f5b18 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 29 Dec 2017 15:49:54 -0500 Subject: [PATCH 11/21] update rst2html --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 13b8b9e3955..dd6ce26ed74 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", - "rst2html": "github:thoward/rst2html#d6e2f21", + "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", diff --git a/yarn.lock b/yarn.lock index 02775740b99..2b013c3c45a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8834,9 +8834,9 @@ rst-selector-parser@^2.2.3: lodash.flattendeep "^4.4.0" nearley "^2.7.10" -"rst2html@github:thoward/rst2html#d6e2f21": +"rst2html@github:thoward/rst2html#990cb89": version "1.0.4" - resolved "https://codeload.github.com/thoward/rst2html/tar.gz/d6e2f219ea94ec7b1fe3cc918d152b67015ab04e" + resolved "https://codeload.github.com/thoward/rst2html/tar.gz/990cb89f2a300cdd9151790be377c4c0840df809" dependencies: restructured "0.0.11" From 92086bc858b3566c926b38a44e3204c271631717 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 5 Jan 2018 15:33:30 -0500 Subject: [PATCH 12/21] fix typo --- public/app/plugins/datasource/graphite/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 0c8e1ca0843..dc5c8db644b 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -345,7 +345,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }); }; - this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, limit, optionalOptions) => { + this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => { let options = optionalOptions || {}; let httpOptions: any = { From 60ba6ee6a223c3b4375642499e68264a9a019e20 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 8 Jan 2018 16:52:28 -0500 Subject: [PATCH 13/21] interpolate variables in tags & values during autocomplete --- public/app/plugins/datasource/graphite/datasource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index dc5c8db644b..15f7c3ef961 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -286,7 +286,7 @@ 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, }; @@ -317,7 +317,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv method: 'GET', url: '/tags/autoComplete/tags', params: { - expr: expressions, + expr: _.map(expressions, expression => templateSrv.replace(expression)), }, // for cancellations requestId: options.requestId, @@ -352,8 +352,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv method: 'GET', url: '/tags/autoComplete/values', params: { - expr: expressions, - tag: tag, + expr: _.map(expressions, expression => templateSrv.replace(expression)), + tag: templateSrv.replace(tag), }, // for cancellations requestId: options.requestId, From b483d42d34635e8f436798d0d331de59d83b14bc Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 11 Jan 2018 11:21:02 -0500 Subject: [PATCH 14/21] fix issue with metric find & functions being loaded multiple times --- public/app/plugins/datasource/graphite/datasource.ts | 4 +++- public/app/plugins/datasource/graphite/query_ctrl.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 15f7c3ef961..b5e5d45e9fb 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -429,7 +429,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv url: '/functions', }; - return self + self.funcDefs = self .doGraphiteRequest(httpOptions) .then(results => { if (results.status !== 200 || typeof results.data !== 'object') { @@ -530,6 +530,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); return self.funcDefs; }); + + return self.funcDefs; }; this.testDatasource = function() { diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index d7bb924a4f6..5482fbcdd08 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -106,7 +106,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { } getAltSegments(index, prefix) { - var query = '*' + prefix + '*'; + var query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*'; if (index > 0) { query = this.queryModel.getSegmentPathUpTo(index) + '.' + query; } From 7e750cef433521c00ccd46a8439472b727f52882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 12 Jan 2018 12:31:27 +0100 Subject: [PATCH 15/21] fix: query editor needs to wait for function definitions to load --- .../plugins/datasource/graphite/datasource.ts | 39 +++++++++++-------- .../plugins/datasource/graphite/query_ctrl.ts | 8 ++-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index b5e5d45e9fb..e33bee610b8 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -14,6 +14,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv this.withCredentials = instanceSettings.withCredentials; this.render_method = instanceSettings.render_method || 'POST'; this.funcDefs = null; + this.funcDefsPromise = null; this.getQueryOptionsInfo = function() { return { @@ -412,16 +413,19 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return gfunc.getFuncDef(name, this.funcDefs); }; - this.getFuncDefs = function() { - let self = this; + this.waitForFuncDefsLoaded = function() { + return this.getFuncDefs(); + }; - if (self.funcDefs !== null) { - return Promise.resolve(self.funcDefs); + this.getFuncDefs = function() { + if (this.funcDefsPromise !== null) { + return this.funcDefsPromise; } - if (!supportsFunctionIndex(self.graphiteVersion)) { - self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); - return Promise.resolve(self.funcDefs); + if (!supportsFunctionIndex(this.graphiteVersion)) { + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + this.funcDefsPromise = Promise.resolve(this.funcDefs); + return this.funcDefsPromise; } let httpOptions = { @@ -429,15 +433,15 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv url: '/functions', }; - self.funcDefs = self - .doGraphiteRequest(httpOptions) + this.funcDefsPromise = this.doGraphiteRequest(httpOptions) .then(results => { if (results.status !== 200 || typeof results.data !== 'object') { - self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); - return Promise.resolve(self.funcDefs); + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + return Promise.resolve(this.funcDefs); } - self.funcDefs = {}; + this.funcDefs = {}; + _.forEach(results.data || {}, (funcDef, funcName) => { // skip graphite graph functions if (funcDef.group === 'Graph') { @@ -522,16 +526,17 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv func.params.push(param); }); - self.funcDefs[funcName] = func; + this.funcDefs[funcName] = func; }); - return self.funcDefs; + return this.funcDefs; }) .catch(err => { - self.funcDefs = gfunc.getFuncDefs(self.graphiteVersion); - return self.funcDefs; + console.log('Fetching graphite functions error', err); + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + return this.funcDefs; }); - return self.funcDefs; + return this.funcDefsPromise; }; this.testDatasource = function() { diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 5482fbcdd08..29ff29b7ae3 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -20,16 +20,16 @@ export class GraphiteQueryCtrl extends QueryCtrl { paused: boolean; /** @ngInject **/ - constructor($scope, $injector, private uiSegmentSrv, private templateSrv) { + constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) { super($scope, $injector); this.supportsTags = this.datasource.supportsTags; this.paused = false; + this.target.target = this.target.target || ''; - if (this.target) { - this.target.target = this.target.target || ''; + this.datasource.waitForFuncDefsLoaded().then(() => { this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv); this.buildSegments(); - } + }); this.removeTagValue = '-- remove tag --'; } From 38f4d5d06d77acf8b06e7375897859bb5a138391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 12 Jan 2018 13:06:29 +0100 Subject: [PATCH 16/21] fix: restored previous behavior of form_dropdown, this fixes all my observerd bugs with the dropdown behavior --- public/app/core/components/form_dropdown/form_dropdown.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index f3d920c5b4d..cf9c6b5efe0 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -176,7 +176,7 @@ export class FormDropdownCtrl { updateValue(text) { text = _.unescape(text); - if ((!this.allowCustom && text === '') || this.text === text) { + if (text === '' || this.text === text) { return; } @@ -225,9 +225,7 @@ export class FormDropdownCtrl { var typeahead = this.inputElement.data('typeahead'); if (typeahead) { - if (!this.allowCustom) { - this.inputElement.val(''); - } + this.inputElement.val(''); typeahead.lookup(); } } From 1a6bf692c6627620972963b9196e78d12959f6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 12 Jan 2018 13:07:50 +0100 Subject: [PATCH 17/21] fix: minor fixes --- public/app/plugins/datasource/graphite/config_ctrl.ts | 1 - public/app/plugins/datasource/graphite/datasource.ts | 3 +-- .../app/plugins/datasource/graphite/partials/query.editor.html | 3 ++- .../app/plugins/datasource/graphite/specs/query_ctrl_specs.ts | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) 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 e33bee610b8..4fce3c33ccd 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -387,8 +387,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv let httpOptions = { method: 'GET', - url: '/version/_', // Prevent last / trimming - // for cancellations + url: '/version', requestId: options.requestId, }; diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index e267238ef96..8ff0b9c7adf 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -16,6 +16,7 @@ lookup-text="false" allow-custom="true" label-mode="true" + placeholder="Tag key" css-class="query-segment-key" get-options="ctrl.getTags($index, $query)" on-change="ctrl.tagChanged(tag, $index)" @@ -36,7 +37,7 @@ allow-custom="true" label-mode="true" css-class="query-segment-value" - placeholder="select tag value" + placeholder="Tag value" get-options="ctrl.getTagValues(tag, $index, $query)" on-change="ctrl.tagChanged(tag, $index)" start-open="!ctrl.showDelimiter($index)" diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts index fbbd9134c3e..d956447e828 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts @@ -26,6 +26,7 @@ describe('GraphiteQueryCtrl', function() { ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0'))); ctx.datasource.getFuncDef = gfunc.getFuncDef; + ctx.datasource.waitForFuncDefsLoaded = ctx.$q.when(null); ctx.datasource.createFuncInstance = gfunc.createFuncInstance; ctx.panelCtrl = { panel: {} }; ctx.panelCtrl = { From e9d33750cb869cbf7b9ff5537294a795051b8fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 12 Jan 2018 14:20:15 +0100 Subject: [PATCH 18/21] fix: graphite func editor fixes, this component is messy and ugly as hell --- .../datasource/graphite/add_graphite_func.js | 234 +++++++++--------- .../datasource/graphite/func_editor.js | 61 +++-- 2 files changed, 152 insertions(+), 143 deletions(-) diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index c66176aeb8f..1d72c2c11eb 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -1,137 +1,130 @@ -define([ - 'angular', - 'lodash', - 'jquery', - 'rst2html', - 'tether-drop', -], -function (angular, _, $, rst2html, Drop) { +define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) { 'use strict'; - 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 $input = $(inputTemplate); - var $button = $(buttonTemplate); + var $input = $(inputTemplate); + var $button = $(buttonTemplate); - $input.appendTo(elem); - $button.appendTo(elem); + $input.appendTo(elem); + $button.appendTo(elem); - ctrl.datasource.getFuncDefs().then(function(funcDefs) { - var allFunctions = _.map(funcDefs, 'name').sort(); + ctrl.datasource.getFuncDefs().then(function(funcDefs) { + var allFunctions = _.map(funcDefs, 'name').sort(); - $scope.functionMenu = createFunctionDropDownMenu(funcDefs); + $scope.functionMenu = createFunctionDropDownMenu(funcDefs); - $input.attr('data-provide', 'typeahead'); - $input.typeahead({ - source: allFunctions, - minLength: 1, - items: 10, - updater: function (value) { - var funcDef = ctrl.datasource.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() { - ctrl.addFunction(funcDef); + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: allFunctions, + minLength: 1, + items: 10, + updater: function(value) { + var funcDef = ctrl.datasource.getFuncDef(value); + if (!funcDef) { + // try find close match + value = value.toLowerCase(); + funcDef = _.find(allFunctions, function(funcName) { + return funcName.toLowerCase().indexOf(value) === 0; }); - $input.trigger('blur'); - return ''; + if (!funcDef) { + return; + } } - }); - $button.click(function() { - $button.hide(); - $input.show(); - $input.focus(); - }); + $scope.$apply(function() { + ctrl.addFunction(funcDef); + }); - $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); + $input.trigger('blur'); + return ''; + }, }); - var drops = []; + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); - $(elem) - .on('mouseenter', 'ul.dropdown-menu li', function () { - while (drops.length > 0) { - drops.pop().remove(); + $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); + }); + + var drop; + var cleanUpDrop = function() { + if (drop) { + drop.destroy(); + drop = null; + } + }; + + $(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 funcDef; - try { - funcDef = ctrl.datasource.getFuncDef($('a', this).text()); - } catch (e) { - // ignore - } + var contentElement = document.createElement('div'); + contentElement.innerHTML = '

' + funcDef.name + '

' + rst2html(shortDesc); - if (funcDef && funcDef.description) { - var shortDesc = funcDef.description; - if (shortDesc.length > 500) { - shortDesc = shortDesc.substring(0, 497) + '...'; - } + 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(); + }); - var contentElement = document.createElement('div'); - contentElement.innerHTML = '

' + funcDef.name + '

' + rst2html(shortDesc); - - var drop = new Drop({ - target: this, - content: contentElement, - classes: 'drop-popover', - openOn: 'always', - tetherOptions: { - attachment: 'bottom left', - targetAttachment: 'bottom right', - }, - }); - - drops.push(drop); - - drop.open(); - } - }) - .on('mouseout', 'ul.dropdown-menu li', function() { - while (drops.length > 0) { - drops.pop().remove(); - } - }); - } - }; - }); + $scope.$on('$destroy', cleanUpDrop); + }, + }; + }); function createFunctionDropDownMenu(funcDefs) { var categories = {}; @@ -149,11 +142,14 @@ function (angular, _, $, rst2html, Drop) { }); }); - return _.sortBy(_.map(categories, function(submenu, category) { - return { - text: category, - submenu: _.sortBy(submenu, 'text') - }; - }), 'text'); + return _.sortBy( + _.map(categories, function(submenu, category) { + return { + text: category, + submenu: _.sortBy(submenu, 'text'), + }; + }), + 'text' + ); } }); diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index af4318b0877..3420d7e97c3 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -32,6 +32,7 @@ function (angular, _, $, rst2html) { var func = $scope.func; var scheduledRelink = false; var paramCountAtLink = 0; + var cancelBlur = null; function clickFuncParam(paramIndex) { /*jshint validthis:true */ @@ -79,41 +80,54 @@ function (angular, _, $, rst2html) { return {}; } - function inputBlur(paramIndex) { + function switchToLink(inputElem, paramIndex) { /*jshint validthis:true */ - var $input = $(this); - if ($input.data('typeahead') && $input.data('typeahead').shown) { - return; - } + var $input = $(inputElem); + + clearTimeout(cancelBlur); + cancelBlur = null; var $link = $input.prev(); var $comma = $link.prev('.comma'); var newValue = $input.val(); + // remove optional empty params if (newValue !== '' || paramDef(paramIndex).optional) { - $link.html(templateSrv.highlightVariablesAsHtml(newValue)); - func.updateParam(newValue, paramIndex); - scheduledRelinkIfNeeded(); - - $scope.$apply(function() { - ctrl.targetChanged(); - }); - - if ($link.hasClass('last') && newValue === '') { - $comma.addClass('last'); - } else { - $link.removeClass('last'); - } - $input.hide(); - $link.show(); } + + $link.html(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(); } } @@ -135,9 +149,8 @@ function (angular, _, $, rst2html) { minLength: 0, items: 20, updater: function (value) { - setTimeout(function() { - inputBlur.call($input[0], paramIndex); - }, 0); + $input.val(value); + switchToLink($input[0], paramIndex); return value; } }); From 315ecf8505bf3348e0c3e400c00ead91fe07c6ca Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 12 Jan 2018 11:56:50 -0500 Subject: [PATCH 19/21] fix tests & some display issues --- public/app/plugins/datasource/graphite/func_editor.js | 2 +- .../plugins/datasource/graphite/partials/query.editor.html | 6 ------ .../plugins/datasource/graphite/specs/query_ctrl_specs.ts | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 3420d7e97c3..949a6912720 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -94,9 +94,9 @@ function (angular, _, $, rst2html) { // remove optional empty params if (newValue !== '' || paramDef(paramIndex).optional) { func.updateParam(newValue, paramIndex); + $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); } - $link.html(templateSrv.highlightVariablesAsHtml(newValue)); scheduledRelinkIfNeeded(); $scope.$apply(function() { diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 8ff0b9c7adf..6414a39c94f 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -40,7 +40,6 @@ placeholder="Tag value" get-options="ctrl.getTagValues(tag, $index, $query)" on-change="ctrl.tagChanged(tag, $index)" - start-open="!ctrl.showDelimiter($index)" />
@@ -56,11 +55,6 @@
-
diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts index d956447e828..f8b70b05940 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts @@ -26,7 +26,7 @@ describe('GraphiteQueryCtrl', function() { ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0'))); ctx.datasource.getFuncDef = gfunc.getFuncDef; - ctx.datasource.waitForFuncDefsLoaded = ctx.$q.when(null); + ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null)); ctx.datasource.createFuncInstance = gfunc.createFuncInstance; ctx.panelCtrl = { panel: {} }; ctx.panelCtrl = { From 50ffe56b6096928ee08aa1072a84f5653e4ccf72 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 16 Jan 2018 17:17:58 -0500 Subject: [PATCH 20/21] remove duplicate sass rules --- public/sass/components/_query_editor.scss | 28 +---------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index a99bd1bbd79..6b2860d57bf 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -90,6 +90,7 @@ } input[type='text'].tight-form-func-param { + font-size: 0.875rem; background: transparent; border: none; margin: 0; @@ -129,33 +130,6 @@ input[type='text'].tight-form-func-param { } } -input[type='text'].tight-form-func-param { - font-size: 0.875rem; - background: transparent; - border: none; - margin: 0; - padding: 0; -} - -.tight-form-func-controls { - display: none; - text-align: center; - - .fa-arrow-left { - float: left; - position: relative; - top: 2px; - } - .fa-arrow-right { - float: right; - position: relative; - top: 2px; - } - .fa-remove { - margin-left: 10px; - } -} - .query-troubleshooter { font-size: $font-size-sm; margin: $gf-form-margin; From c62b0858aec0ccff0986fee33705a7193e579044 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 16 Jan 2018 18:04:59 -0500 Subject: [PATCH 21/21] move graphite /functions parsing into gfunc.ts --- .../plugins/datasource/graphite/datasource.ts | 92 +----------------- .../app/plugins/datasource/graphite/gfunc.ts | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+), 90 deletions(-) diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 4fce3c33ccd..f02945b8969 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -436,97 +436,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv .then(results => { if (results.status !== 200 || typeof results.data !== 'object') { this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); - return Promise.resolve(this.funcDefs); + } else { + this.funcDefs = gfunc.parseFuncDefs(results.data); } - - this.funcDefs = {}; - - _.forEach(results.data || {}, (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); - }); - - this.funcDefs[funcName] = func; - }); return this.funcDefs; }) .catch(err => { diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index 8207a70b868..3d33d0f1005 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -1077,8 +1077,103 @@ function getFuncDefs(graphiteVersion, idx?) { 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); + }); + + funcDefs[funcName] = func; + }); + + return funcDefs; +} + export default { createFuncInstance: createFuncInstance, getFuncDef: getFuncDef, getFuncDefs: getFuncDefs, + parseFuncDefs: parseFuncDefs, };