Commit 3a4e0513 by Dan Cech

support for loading function definitions from graphite

parent 307b419f
......@@ -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
......
......@@ -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",
......
......@@ -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);
});
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$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);
});
$input.blur(function() {
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
function getAllFunctionNames(categories) {
return _.reduce(categories, function(list, category) {
_.each(category, function(func) {
list.push(func.name);
});
return list;
}, []);
}
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
var submenu = _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
};
function createFunctionDropDownMenu(funcDefs) {
var categories = {};
_.forEach(funcDefs, function(funcDef) {
if (!funcDef.category) {
return;
}
if (!categories[funcDef.category]) {
categories[funcDef.category] = [];
}
categories[funcDef.category].push({
text: funcDef.name,
click: "ctrl.addFunction('" + funcDef.name + "')",
});
});
return _.sortBy(_.map(categories, function(submenu, category) {
return {
text: category,
submenu: submenu
submenu: _.sortBy(submenu, 'text')
};
});
}), 'text');
}
});
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');
}
......@@ -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 = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
' class="input-small tight-form-func-param"></input>';
var funcControlsTemplate =
'<div class="tight-form-func-controls">' +
......@@ -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);
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 last = (index >= func.params.length - 1) && param.optional && !paramValue;
if (last && param.multiple) {
paramValue = '+';
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
$('<span class="comma' + (last ? ' last' : '') + '">, </span>').appendTo(elem);
}
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $paramLink = $(
'<a ng-click="" class="graphite-func-param-link' + (last ? ' last' : '') + '">'
+ (paramValue || '&nbsp;') + '</a>');
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);
}
});
$('<span>)</span>').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();
}
......
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);
......
......@@ -18,7 +18,7 @@
<gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
get-options="ctrl.getTagOperators()"
on-change="ctrl.tagChanged(tag, $index)"
min-input-width="30">
min-input-width="30">
</gf-form-dropdown>
<gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
get-options="ctrl.getTagValues(tag, $index, $query)"
......
......@@ -2,7 +2,6 @@ import './add_graphite_func';
import './func_editor';
import _ from 'lodash';
import gfunc from './gfunc';
import GraphiteQuery from './graphite_query';
import { QueryCtrl } from 'app/plugins/sdk';
import appEvents from 'app/core/app_events';
......@@ -26,7 +25,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
if (this.target) {
this.target.target = this.target.target || '';
this.queryModel = new GraphiteQuery(this.target, templateSrv);
this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
this.buildSegments();
}
......@@ -242,7 +241,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}
addFunction(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, {
var newFunc = this.datasource.createFuncInstance(funcDef, {
withDefaultParams: true,
});
newFunc.added = true;
......@@ -268,8 +267,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
}
addSeriesByTagFunc(tag) {
let funcDef = gfunc.getFuncDef('seriesByTag');
let newFunc = gfunc.createFuncInstance(funcDef, {
let newFunc = this.datasource.createFuncInstance('seriesByTag', {
withDefaultParams: false,
});
let tagParam = `${tag}=select tag value`;
......
......@@ -5,7 +5,8 @@ describe('when creating func instance from func names', function() {
var func = gfunc.createFuncInstance('sumSeries');
expect(func).toBeTruthy();
expect(func.def.name).toEqual('sumSeries');
expect(func.def.params.length).toEqual(5);
expect(func.def.params.length).toEqual(1);
expect(func.def.params[0].multiple).toEqual(true);
expect(func.def.defaultParams.length).toEqual(1);
});
......@@ -74,10 +75,10 @@ describe('when rendering func instance', function() {
});
});
describe('when requesting function categories', function() {
it('should return function categories', function() {
var catIndex = gfunc.getCategories('1.0');
expect(catIndex.Special.length).toBeGreaterThan(8);
describe('when requesting function definitions', function() {
it('should return function definitions', function() {
var funcIndex = gfunc.getFuncDefs('1.0');
expect(Object.keys(funcIndex).length).toBeGreaterThan(8);
});
});
......
......@@ -24,6 +24,9 @@ describe('GraphiteQueryCtrl', function() {
ctx.scope = $rootScope.$new();
ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
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.createFuncInstance = gfunc.createFuncInstance;
ctx.panelCtrl = { panel: {} };
ctx.panelCtrl = {
panel: {
......@@ -180,7 +183,21 @@ describe('GraphiteQueryCtrl', function() {
ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
ctx.ctrl.parseTarget();
});
it('should add function params', function() {
expect(ctx.ctrl.queryModel.segments.length).to.be(1);
expect(ctx.ctrl.queryModel.segments[0].value).to.be('#A');
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(60);
});
it('target should remain the same', function() {
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
});
it('targetFull should include nested queries', function() {
ctx.ctrl.panelCtrl.panel.targets = [
{
target: 'nested.query.count',
......@@ -189,13 +206,9 @@ describe('GraphiteQueryCtrl', function() {
];
ctx.ctrl.updateModelTarget();
});
it('target should remain the same', function() {
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
});
it('targetFull should include nexted queries', function() {
expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
});
});
......
......@@ -130,6 +130,7 @@ input[type="text"].tight-form-func-param {
}
input[type="text"].tight-form-func-param {
font-size: 0.875rem;
background: transparent;
border: none;
margin: 0;
......
......@@ -6,4 +6,12 @@
min-width: 100px;
text-align: center;
}
.last {
display: none;
}
&:hover .last {
display: inline;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment