Commit 12b08b61 by Torkel Ödegaard

Merge remote-tracking branch 'origin/graphite-query-editor-enhancements'

parents 3662b03d c62b0858
......@@ -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
......
......@@ -134,7 +134,7 @@
"clipboard": "^1.7.1",
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.1.1",
"eventemitter3": "^2.0.2",
"eventemitter3": "^2.0.3",
"file-saver": "^1.3.3",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
......@@ -153,6 +153,7 @@
"react-select": "^1.1.0",
"react-sizeme": "^2.3.6",
"remarkable": "^1.7.1",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^5.4.3",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop",
......
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core_module';
function typeaheadMatcher(item) {
var str = this.query;
if (str === '') {
return true;
}
if (str[0] === '/') {
str = str.substring(1);
}
......@@ -30,6 +32,8 @@ export class FormDropdownCtrl {
getOptions: any;
optionCache: any;
lookupText: boolean;
placeholder: any;
startOpen: any;
/** @ngInject **/
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
......@@ -47,6 +51,10 @@ export class FormDropdownCtrl {
this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
}
if (this.placeholder) {
this.inputElement.attr('placeholder', this.placeholder);
}
this.inputElement.attr('data-provide', 'typeahead');
this.inputElement.typeahead({
source: this.typeaheadSource.bind(this),
......@@ -61,8 +69,7 @@ export class FormDropdownCtrl {
var typeahead = this.inputElement.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
this.source(this.query, this.process.bind(this));
};
this.linkElement.keydown(evt => {
......@@ -81,6 +88,10 @@ export class FormDropdownCtrl {
});
this.inputElement.blur(this.inputBlur.bind(this));
if (this.startOpen) {
setTimeout(this.open.bind(this), 0);
}
}
getOptionsInternal(query) {
......@@ -121,9 +132,9 @@ export class FormDropdownCtrl {
});
// add custom values
if (this.allowCustom) {
if (this.allowCustom && this.text !== '') {
if (_.indexOf(optionTexts, this.text) === -1) {
options.unshift(this.text);
optionTexts.unshift(this.text);
}
}
......@@ -228,10 +239,10 @@ const template = `
style="display:none">
</input>
<a ng-class="ctrl.cssClasses"
tabindex="1"
ng-click="ctrl.open()"
give-focus="ctrl.focus"
ng-bind-html="ctrl.display">
tabindex="1"
ng-click="ctrl.open()"
give-focus="ctrl.focus"
ng-bind-html="ctrl.display || '&nbsp;'">
</a>
`;
......@@ -250,6 +261,8 @@ export function formDropdownDirective() {
allowCustom: '@',
labelMode: '@',
lookupText: '@',
placeholder: '@',
startOpen: '@',
},
};
}
......
......@@ -12,7 +12,7 @@ function (_, $, coreModule) {
' class="gf-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>';
......
......@@ -106,10 +106,6 @@ function (angular, _, coreModule) {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
};
this.newSelectTagValue = function() {
return new MetricSegment({value: 'select tag value', fake: true});
};
});
});
......@@ -16,12 +16,12 @@
Add variable
</a>
<div class="grafana-info-box">
<h5>What does variables do?</h5>
<p>Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
<h5>What do variables do?</h5>
<p>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
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
Templating documentation
</a> for more information.
......@@ -93,7 +93,7 @@
</div>
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span>
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
</div>
<div class="gf-form-inline">
......
define([
'angular',
'lodash',
'jquery',
'./gfunc',
],
function (angular, _, $, gfunc) {
define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) {
'use strict';
gfunc = gfunc.default;
angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) {
var inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
angular
.module('grafana.directives')
.directive('graphiteAddFunc', function($compile) {
var inputTemplate = '<input type="text"'+
' class="gf-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate =
'<a class="gf-form-label query-part dropdown-toggle"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<i class="fa fa-plus"></i></a>';
var buttonTemplate = '<a class="gf-form-label query-part dropdown-toggle"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<i class="fa fa-plus"></i></a>';
return {
link: function($scope, elem) {
var ctrl = $scope.ctrl;
return {
link: function($scope, elem) {
var ctrl = $scope.ctrl;
var graphiteVersion = ctrl.datasource.graphiteVersion;
var categories = gfunc.getCategories(graphiteVersion);
var allFunctions = getAllFunctionNames(categories);
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$scope.functionMenu = createFunctionDropDownMenu(categories);
$input.appendTo(elem);
$button.appendTo(elem);
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
ctrl.datasource.getFuncDefs().then(function(funcDefs) {
var allFunctions = _.map(funcDefs, 'name').sort();
$scope.functionMenu = createFunctionDropDownMenu(funcDefs);
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: allFunctions,
minLength: 1,
items: 10,
updater: function (value) {
var funcDef = gfunc.getFuncDef(value);
updater: function(value) {
var funcDef = ctrl.datasource.getFuncDef(value);
if (!funcDef) {
// try find close match
value = value.toLowerCase();
......@@ -48,7 +39,9 @@ function (angular, _, $, gfunc) {
return funcName.toLowerCase().indexOf(value) === 0;
});
if (!funcDef) { return; }
if (!funcDef) {
return;
}
}
$scope.$apply(function() {
......@@ -57,7 +50,7 @@ function (angular, _, $, gfunc) {
$input.trigger('blur');
return '';
}
},
});
$button.click(function() {
......@@ -82,32 +75,81 @@ function (angular, _, $, gfunc) {
});
$compile(elem.contents())($scope);
}
};
});
});
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) + '...';
}
function getAllFunctionNames(categories) {
return _.reduce(categories, function(list, category) {
_.each(category, function(func) {
list.push(func.name);
var contentElement = document.createElement('div');
contentElement.innerHTML = '<h4>' + funcDef.name + '</h4>' + rst2html(shortDesc);
drop = new Drop({
target: this,
content: contentElement,
classes: 'drop-popover',
openOn: 'always',
tetherOptions: {
attachment: 'bottom left',
targetAttachment: 'bottom right',
},
});
}
})
.on('mouseout', 'ul.dropdown-menu li', function() {
cleanUpDrop();
});
$scope.$on('$destroy', cleanUpDrop);
},
};
});
function createFunctionDropDownMenu(funcDefs) {
var categories = {};
_.forEach(funcDefs, function(funcDef) {
if (!funcDef.category) {
return;
}
if (!categories[funcDef.category]) {
categories[funcDef.category] = [];
}
categories[funcDef.category].push({
text: funcDef.name,
click: "ctrl.addFunction('" + funcDef.name + "')",
});
return list;
}, []);
}
});
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
var submenu = _.map(list, function(value) {
return _.sortBy(
_.map(categories, function(submenu, category) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
text: category,
submenu: _.sortBy(submenu, 'text'),
};
});
return {
text: category,
submenu: submenu
};
});
}),
'text'
);
}
});
......@@ -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();
}
......
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
import gfunc from './gfunc';
/** @ngInject */
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
......@@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
this.funcDefs = null;
this.funcDefsPromise = null;
this.getQueryOptionsInfo = function() {
return {
......@@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
let options = optionalOptions || {};
let interpolatedQuery = templateSrv.replace(query);
// special handling for tag_values(<tag>[,<expression>]*), 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(<expression>[,<expression>]*), this is used for template variables
matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/);
if (matches) {
const expressions = [];
if (matches[1]) {
expressions.push(matches[1]);
const exprRegex = /, *([^,]+)/g;
let match;
while ((match = exprRegex.exec(matches[2])) !== null) {
expressions.push(match[1]);
}
}
options.limit = 10000;
return this.getTagsAutoComplete(expressions, undefined, options);
}
let httpOptions: any = {
method: 'GET',
url: '/metrics/find',
......@@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
requestId: options.requestId,
};
if (options && options.range) {
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
......@@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
requestId: options.requestId,
};
if (options && options.range) {
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
......@@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
let httpOptions: any = {
method: 'GET',
url: '/tags/' + tag,
url: '/tags/' + templateSrv.replace(tag),
// for cancellations
requestId: options.requestId,
};
if (options && options.range) {
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
......@@ -279,18 +311,29 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
});
};
this.getTagsAutoComplete = (expression, tagPrefix) => {
this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => {
let options = optionalOptions || {};
let httpOptions: any = {
method: 'GET',
url: '/tags/autoComplete/tags',
params: {
expr: expression,
expr: _.map(expressions, expression => templateSrv.replace(expression)),
},
// for cancellations
requestId: options.requestId,
};
if (tagPrefix) {
httpOptions.params.tagPrefix = tagPrefix;
}
if (options.limit) {
httpOptions.params.limit = options.limit;
}
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) {
......@@ -303,19 +346,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
});
};
this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => {
this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => {
let options = optionalOptions || {};
let httpOptions: any = {
method: 'GET',
url: '/tags/autoComplete/values',
params: {
expr: expression,
tag: tag,
expr: _.map(expressions, expression => templateSrv.replace(expression)),
tag: templateSrv.replace(tag),
},
// for cancellations
requestId: options.requestId,
};
if (valuePrefix) {
httpOptions.params.valuePrefix = valuePrefix;
}
if (options.limit) {
httpOptions.params.limit = options.limit;
}
if (options.range) {
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => {
if (results.data) {
......@@ -328,10 +382,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
});
};
this.getVersion = function() {
this.getVersion = function(optionalOptions) {
let options = optionalOptions || {};
let httpOptions = {
method: 'GET',
url: '/version/_', // Prevent last / trimming
url: '/version',
requestId: options.requestId,
};
return this.doGraphiteRequest(httpOptions)
......@@ -347,6 +404,52 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
});
};
this.createFuncInstance = function(funcDef, options?) {
return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
};
this.getFuncDef = function(name) {
return gfunc.getFuncDef(name, this.funcDefs);
};
this.waitForFuncDefsLoaded = function() {
return this.getFuncDefs();
};
this.getFuncDefs = function() {
if (this.funcDefsPromise !== null) {
return this.funcDefsPromise;
}
if (!supportsFunctionIndex(this.graphiteVersion)) {
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
this.funcDefsPromise = Promise.resolve(this.funcDefs);
return this.funcDefsPromise;
}
let httpOptions = {
method: 'GET',
url: '/functions',
};
this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
.then(results => {
if (results.status !== 200 || typeof results.data !== 'object') {
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
} else {
this.funcDefs = gfunc.parseFuncDefs(results.data);
}
return this.funcDefs;
})
.catch(err => {
console.log('Fetching graphite functions error', err);
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
return this.funcDefs;
});
return this.funcDefsPromise;
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function() {
return { status: 'success', message: 'Data source is working' };
......@@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
function supportsTags(version: string): boolean {
return isVersionGtOrEq(version, '1.1');
}
function supportsFunctionIndex(version: string): boolean {
return isVersionGtOrEq(version, '1.1');
}
......@@ -2,17 +2,18 @@ define([
'angular',
'lodash',
'jquery',
'rst2html',
],
function (angular, _, $) {
function (angular, _, $, rst2html) {
'use strict';
angular
.module('grafana.directives')
.directive('graphiteFuncEditor', function($compile, templateSrv) {
.directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) {
var funcSpanTemplate = '<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,19 +30,20 @@ function (angular, _, $) {
var $funcControls = $(funcControlsTemplate);
var ctrl = $scope.ctrl;
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
var paramCountAtLink = 0;
var cancelBlur = null;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $comma = $link.prev('.comma');
var $input = $link.next();
$input.val(func.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$comma.removeClass('last');
$link.hide();
$input.show();
$input.focus();
......@@ -68,31 +70,64 @@ function (angular, _, $) {
}
}
function inputBlur(paramIndex) {
function paramDef(index) {
if (index < func.def.params.length) {
return func.def.params[index];
}
if (_.last(func.def.params).multiple) {
return _.assign({}, _.last(func.def.params), {optional: true});
}
return {};
}
function switchToLink(inputElem, paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $input = $(inputElem);
clearTimeout(cancelBlur);
cancelBlur = null;
var $link = $input.prev();
var $comma = $link.prev('.comma');
var newValue = $input.val();
if (newValue !== '' || func.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
// remove optional empty params
if (newValue !== '' || paramDef(paramIndex).optional) {
func.updateParam(newValue, paramIndex);
$link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
}
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
scheduledRelinkIfNeeded();
$scope.$apply(function() {
ctrl.targetChanged();
});
$scope.$apply(function() {
ctrl.targetChanged();
});
$input.hide();
$link.show();
if ($link.hasClass('last') && newValue === '') {
$comma.addClass('last');
} else {
$link.removeClass('last');
}
$input.hide();
$link.show();
}
// this = input element
function inputBlur(paramIndex) {
/*jshint validthis:true */
var inputElem = this;
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout(function() {
switchToLink(inputElem, paramIndex);
}, 200);
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
$(this).blur();
}
}
......@@ -104,8 +139,8 @@ function (angular, _, $) {
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
var options = funcDef.params[paramIndex].options;
if (funcDef.params[paramIndex].type === 'int') {
var options = paramDef(paramIndex).options;
if (paramDef(paramIndex).type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
......@@ -114,9 +149,8 @@ function (angular, _, $) {
minLength: 0,
items: 20,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
$input.val(value);
switchToLink($input[0], paramIndex);
return value;
}
});
......@@ -148,18 +182,34 @@ function (angular, _, $) {
$funcControls.appendTo(elem);
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length <= index) {
return;
var defParams = _.clone(func.def.params);
var lastParam = _.last(func.def.params);
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 +221,9 @@ function (angular, _, $) {
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (funcDef.params[index].options) {
if (param.options) {
addTypeahead($input, index);
}
});
$('<span>)</span>').appendTo(elem);
......@@ -182,7 +231,7 @@ function (angular, _, $) {
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFistParam() {
function ifJustAddedFocusFirstParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(function() {
......@@ -223,7 +272,20 @@ function (angular, _, $) {
}
if ($target.hasClass('fa-question-circle')) {
window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
var funcDef = ctrl.datasource.getFuncDef(func.def.name);
if (funcDef && funcDef.description) {
popoverSrv.show({
element: e.target,
position: 'bottom left',
classNames: 'drop-popover drop-function-def',
template: '<div style="overflow:auto;max-height:30rem;">'
+ '<h4>' + funcDef.name + '</h4>' + rst2html(funcDef.description) + '</div>',
openOn: 'click',
});
} else {
window.open(
"http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank');
}
return;
}
});
......@@ -233,7 +295,7 @@ function (angular, _, $) {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
ifJustAddedFocusFirstParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
......
import _ from 'lodash';
import gfunc from './gfunc';
import { Parser } from './parser';
export default class GraphiteQuery {
datasource: any;
target: any;
functions: any[];
segments: any[];
......@@ -15,7 +15,8 @@ export default class GraphiteQuery {
scopedVars: any;
/** @ngInject */
constructor(target, templateSrv?, scopedVars?) {
constructor(datasource, target, templateSrv?, scopedVars?) {
this.datasource = datasource;
this.target = target;
this.parseTarget();
......@@ -86,7 +87,7 @@ export default class GraphiteQuery {
switch (astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, {
var innerFunc = this.datasource.createFuncInstance(astNode.name, {
withDefaultParams: false,
});
_.each(astNode.params, param => {
......@@ -133,7 +134,7 @@ export default class GraphiteQuery {
moveAliasFuncLast() {
var aliasFunc = _.find(this.functions, function(func) {
return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric';
return func.def.name.startsWith('alias');
});
if (aliasFunc) {
......@@ -143,7 +144,7 @@ export default class GraphiteQuery {
}
addFunctionParameter(func, value) {
if (func.params.length >= func.def.params.length) {
if (func.params.length >= func.def.params.length && !_.get(_.last(func.def.params), 'multiple', false)) {
throw { message: 'too many parameters for function ' + func.def.name };
}
func.params.push(value);
......@@ -208,7 +209,7 @@ export default class GraphiteQuery {
}
splitSeriesByTagParams(func) {
const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
return _.flatten(
_.map(func.params, (param: string) => {
let matches = tagPattern.exec(param);
......
......@@ -10,30 +10,50 @@
<label class="gf-form-label width-6 query-keyword">Series</label>
</div>
<div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
<gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
<gf-form-dropdown
model="tag.key"
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)">
</gf-form-dropdown>
<gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
on-change="ctrl.tagChanged(tag, $index)"
/>
<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">
</gf-form-dropdown>
<gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
min-input-width="30"
/>
<gf-form-dropdown
model="tag.value"
lookup-text="false"
allow-custom="true"
label-mode="true"
css-class="query-segment-value"
placeholder="Tag value"
get-options="ctrl.getTagValues(tag, $index, $query)"
on-change="ctrl.tagChanged(tag, $index)">
</gf-form-dropdown>
on-change="ctrl.tagChanged(tag, $index)"
/>
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
</div>
<div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
</div>
<div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
</div>
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments()" on-change="ctrl.addNewTag(segment)">
</metric-segment>
<div ng-if="ctrl.paused" class="gf-form">
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><i class="fa fa-play"></i></a>
</div>
<div class="gf-form gf-form--grow">
......
......@@ -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';
......@@ -18,17 +17,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
addTagSegments: any[];
removeTagValue: string;
supportsTags: boolean;
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.queryModel = new GraphiteQuery(this.target, templateSrv);
this.datasource.waitForFuncDefsLoaded().then(() => {
this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
this.buildSegments();
}
});
this.removeTagValue = '-- remove tag --';
}
......@@ -104,8 +105,11 @@ export class GraphiteQueryCtrl extends QueryCtrl {
});
}
getAltSegments(index) {
var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*';
getAltSegments(index, prefix) {
var query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
if (index > 0) {
query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
}
var options = {
range: this.panelCtrl.range,
requestId: 'get-alt-segments',
......@@ -121,7 +125,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
});
});
if (altSegments.length === 0) {
if (index > 0 && altSegments.length === 0) {
return altSegments;
}
......@@ -158,7 +162,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;
}
......@@ -168,8 +172,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;
......@@ -192,6 +196,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
if (segment.type === 'tag') {
let tag = removeTagPrefix(segment.value);
this.pause();
this.addSeriesByTagFunc(tag);
return;
}
......@@ -236,13 +241,13 @@ 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();
}
}
addFunction(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, {
var newFunc = this.datasource.createFuncInstance(funcDef, {
withDefaultParams: true,
});
newFunc.added = true;
......@@ -268,11 +273,10 @@ 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`;
let tagParam = `${tag}=`;
newFunc.params = [tagParam];
this.queryModel.addFunction(newFunc);
newFunc.added = true;
......@@ -314,9 +318,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,
......@@ -355,7 +359,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();
......@@ -374,6 +378,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) {
......
......@@ -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,10 @@ 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.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
ctx.panelCtrl = { panel: {} };
ctx.panelCtrl = {
panel: {
......@@ -180,7 +184,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 +207,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)');
});
});
......@@ -271,12 +285,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);
});
});
......
......@@ -89,7 +89,8 @@
}
}
input[type="text"].tight-form-func-param {
input[type='text'].tight-form-func-param {
font-size: 0.875rem;
background: transparent;
border: none;
margin: 0;
......@@ -129,32 +130,6 @@ input[type="text"].tight-form-func-param {
}
}
input[type="text"].tight-form-func-param {
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;
......@@ -176,3 +151,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;
}
......@@ -6,4 +6,12 @@
min-width: 100px;
text-align: center;
}
.last {
display: none;
}
&:hover .last {
display: inline;
}
}
......@@ -19,6 +19,8 @@ module.exports = function(grunt) {
]);
grunt.registerTask('precommit', [
'jscs',
'jshint',
'sasslint',
'exec:tslint',
'no-only-tests'
......
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