poc: influxdb editor v3 test

This commit is contained in:
Torkel Ödegaard
2015-09-29 18:00:15 +02:00
parent 8a39b32b5c
commit e70b99a706
4 changed files with 452 additions and 48 deletions

View File

@@ -1,4 +1,4 @@
<div class="tight-form-container-no-item-borders">
<div class="">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
@@ -48,26 +48,22 @@
<li>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
</li>
<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
<div style="padding: 10px" ng-if="target.rawQuery">
<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
</div>
</div>
<div ng-hide="target.rawQuery">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-repeat="field in target.fields">
<ul class="tight-form-list">
@@ -75,19 +71,11 @@
<span ng-show="$index === 0">SELECT</span>
</li>
<li>
<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()"></metric-segment>
</li>
<li>
<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()" css-class="tight-form-item-large"></metric-segment>
</li>
<li>
<input type="text" class="tight-form-clear-input text-center" style="width: 70px;" ng-model="field.mathExpr" spellcheck='false' placeholder="math expr" ng-blur="get_data()">
</li>
<li class="tight-form-item query-keyword">
AS
</li>
<li>
<input type="text" class="tight-form-clear-input" style="width: 180px;" ng-model="field.asExpr" spellcheck='false' placeholder="as expr" ng-blur="get_data()">
<li ng-repeat="func in field.parts">
<span influx-query-part-editor class="tight-form-item tight-form-func">
</span>
</li>
</ul>
@@ -108,30 +96,29 @@
<span ng-show="$index === 0">GROUP BY</span>
</li>
<li ng-if="groupBy.type === 'time'">
<span class="tight-form-item">time</span>
<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
</metric-segment>
</li>
<li class="dropdown" ng-if="groupBy.type === 'time'">
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
<span ng-show="target.fill">
fill ({{target.fill}})
</span>
<span ng-show="!target.fill">
no fill
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="setFill('')">no fill</a></li>
<li><a ng-click="setFill('0')">fill (0)</a></li>
<li><a ng-click="setFill('null')">fill (null)</a></li>
<li><a ng-click="setFill('none')">fill (none)</a></li>
<li><a ng-click="setFill('previous')">fill (previous)</a></li>
</ul>
</li>
<li ng-if="groupBy.type === 'tag'">
<metric-segment-model property="groupBy.key" get-options="getTagOptions()" on-change="get_data()"></metric-segment>
<span influx-query-part-editor class="tight-form-item tight-form-func">
</span>
</li>
<!-- <li class="dropdown" ng&#45;if="groupBy.type === 'time'"> -->
<!-- <a class="tight&#45;form&#45;item pointer" data&#45;toggle="dropdown" bs&#45;tooltip="'Insert missing values, important when stacking'" data&#45;placement="right"> -->
<!-- <span ng&#45;show="target.fill"> -->
<!-- fill ({{target.fill}}) -->
<!-- </span> -->
<!-- <span ng&#45;show="!target.fill"> -->
<!-- no fill -->
<!-- </span> -->
<!-- </a> -->
<!-- <ul class="dropdown&#45;menu"> -->
<!-- <li><a ng&#45;click="setFill('')">no fill</a></li> -->
<!-- <li><a ng&#45;click="setFill('0')">fill (0)</a></li> -->
<!-- <li><a ng&#45;click="setFill('null')">fill (null)</a></li> -->
<!-- <li><a ng&#45;click="setFill('none')">fill (none)</a></li> -->
<!-- <li><a ng&#45;click="setFill('previous')">fill (previous)</a></li> -->
<!-- </ul> -->
<!-- </li> -->
<!-- <li ng&#45;if="groupBy.type === 'tag'"> -->
<!-- <metric&#45;segment&#45;model property="groupBy.key" get&#45;options="getTagOptions()" on&#45;change="get_data()"></metric&#45;segment> -->
<!-- </li> -->
</ul>
<ul class="tight-form-list pull-right">
@@ -146,6 +133,8 @@
</div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">

View File

@@ -2,8 +2,10 @@ define([
'angular',
'lodash',
'./query_builder',
'./query_part',
'./query_part_editor',
],
function (angular, _, InfluxQueryBuilder) {
function (angular, _, InfluxQueryBuilder, queryPart) {
'use strict';
var module = angular.module('grafana.controllers');
@@ -17,6 +19,14 @@ function (angular, _, InfluxQueryBuilder) {
target.tags = target.tags || [];
target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
target.fields[0].parts = [
queryPart.create('mean', { withDefaultParams: true }),
queryPart.create('derivate', { withDefaultParams: true }),
queryPart.create('math', { withDefaultParams: true }),
queryPart.create('alias', { withDefaultParams: true }),
];
$scope.func = queryPart.create('time', { withDefaultParams: true });
$scope.queryBuilder = new InfluxQueryBuilder(target);

View File

@@ -0,0 +1,161 @@
define([
'lodash',
'jquery'
],
function (_, $) {
'use strict';
var index = [];
var categories = {
Combine: [],
Transform: [],
Calculate: [],
Filter: [],
Special: []
};
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;
}
addFuncDef({
name: 'mean',
category: categories.Transform,
params: [],
defaultParams: [],
});
addFuncDef({
name: 'derivate',
category: categories.Transform,
params: [{ name: "rate", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
defaultParams: ['10s'],
});
addFuncDef({
name: 'time',
category: categories.Transform,
params: [{ name: "rate", type: "interval", options: ['$interval', '1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
defaultParams: ['$interval'],
});
addFuncDef({
name: 'math',
category: categories.Transform,
params: [{ name: "expr", type: "string"}],
defaultParams: [' / 100'],
});
addFuncDef({
name: 'alias',
category: categories.Transform,
params: [{ name: "name", type: "string"}],
defaultParams: ['alias'],
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
function FuncInstance(funcDef, options) {
this.def = funcDef;
this.params = [];
if (options && options.withDefaultParams) {
this.params = funcDef.defaultParams.slice(0);
}
this.updateText();
}
FuncInstance.prototype.render = function(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)) {
return value;
}
return "'" + value + "'";
}, this);
if (metricExp) {
parameters.unshift(metricExp);
}
return str + parameters.join(', ') + ')';
};
FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
};
FuncInstance.prototype.updateParam = function(strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
this.params[index] = strValue;
}
this.updateText();
};
FuncInstance.prototype.updateText = function () {
if (this.params.length === 0) {
this.text = this.def.name + '()';
return;
}
var text = this.def.name + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
};
return {
create: function(funcDef, options) {
if (_.isString(funcDef)) {
if (!index[funcDef]) {
throw { message: 'Method not found ' + name };
}
funcDef = index[funcDef];
}
return new FuncInstance(funcDef, options);
},
getFuncDef: function(name) {
return index[name];
},
getCategories: function() {
return categories;
}
};
});

View File

@@ -0,0 +1,244 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('influxQueryPartEditor', function($compile, templateSrv) {
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>';
var 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) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(funcControlsTemplate);
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
var paramCountAtLink = 0;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(func.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var 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 inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || func.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
$scope.$apply($scope.targetChanged);
}
$input.hide();
$link.show();
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
var options = funcDef.params[paramIndex].options;
if (funcDef.params[paramIndex].type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function toggleFuncControls() {
var 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);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
}
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $input = $(paramTemplate);
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 (funcDef.params[index].options) {
addTypeahead($input, index);
}
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFistParam() {
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) {
var $target = $(e.target);
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
$scope.removeFunction($scope.func);
});
return;
}
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index - 1);
$scope.targetChanged();
});
return;
}
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index + 1);
$scope.targetChanged();
});
return;
}
if ($target.hasClass('fa-question-circle')) {
window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
return;
}
});
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
}
};
});
});