The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/datasource/graphite/func_editor.ts

319 lines
9.1 KiB

import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import rst2html from 'rst2html';
/** @ngInject */
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
const paramTemplate =
'<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
const funcControlsTemplate = `
<div class="tight-form-func-controls">
<span class="pointer fa fa-arrow-left"></span>
<span class="pointer fa fa-question-circle"></span>
<span class="pointer fa fa-remove" ></span>
<span class="pointer fa fa-arrow-right"></span>
</div>`;
return {
restrict: 'A',
link: function postLink($scope, elem) {
const $funcLink = $(funcSpanTemplate);
const $funcControls = $(funcControlsTemplate);
const ctrl = $scope.ctrl;
const func = $scope.func;
let scheduledRelink = false;
let paramCountAtLink = 0;
let cancelBlur = null;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
const $link = $(this);
const $comma = $link.prev('.comma');
const $input = $link.next();
$input.val(func.params[paramIndex]);
$comma.removeClass('query-part__last');
$link.hide();
$input.show();
$input.focus();
$input.select();
const typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(function() {
relink();
scheduledRelink = false;
}, 200);
}
}
function 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 */
const $input = $(inputElem);
clearTimeout(cancelBlur);
cancelBlur = null;
const $link = $input.prev();
const $comma = $link.prev('.comma');
const newValue = $input.val();
// remove optional empty params
if (newValue !== '' || paramDef(paramIndex).optional) {
func.updateParam(newValue, paramIndex);
$link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
}
scheduledRelinkIfNeeded();
$scope.$apply(function() {
ctrl.targetChanged();
});
if ($link.hasClass('query-part__last') && newValue === '') {
$comma.addClass('query-part__last');
} else {
$link.removeClass('query-part__last');
}
$input.hide();
$link.show();
}
// this = input element
function inputBlur(paramIndex) {
/*jshint validthis:true */
const 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) {
$(this).blur();
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
let options = paramDef(paramIndex).options;
if (paramDef(paramIndex).type === 'int') {
options = _.map(options, function(val) {
return val.toString();
});
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: function(value) {
$input.val(value);
switchToLink($input[0], paramIndex);
return value;
},
});
const typeahead = $input.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function toggleFuncControls() {
const targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$funcControls.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$funcControls.show();
}
function addElementsAndCompile() {
$funcControls.appendTo(elem);
$funcLink.appendTo(elem);
const defParams = _.clone(func.def.params);
const 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;
}
let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
const last = index >= func.params.length - 1 && param.optional && !paramValue;
if (last && param.multiple) {
paramValue = '+';
}
if (index > 0) {
$('<span class="comma' + (last ? ' query-part__last' : '') + '">, </span>').appendTo(elem);
}
const $paramLink = $(
'<a ng-click="" class="graphite-func-param-link' +
(last ? ' query-part__last' : '') +
'">' +
(paramValue || '&nbsp;') +
'</a>'
);
const $input = $(paramTemplate);
$input.attr('placeholder', param.name);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (param.options) {
addTypeahead($input, index);
}
return true;
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFirstParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(function() {
elem
.find('.graphite-func-param-link')
.first()
.click();
}, 10);
}
}
function registerFuncControlsToggle() {
$funcLink.click(toggleFuncControls);
}
function registerFuncControlsActions() {
$funcControls.click(function(e) {
const $target = $(e.target);
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
ctrl.removeFunction($scope.func);
});
return;
}
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-question-circle')) {
const 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;
}
});
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFirstParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
},
};
}
angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor);