feat(query part): moved query part editor from influxdb to core

This commit is contained in:
Torkel Ödegaard
2016-05-11 14:52:44 +02:00
parent cca37caf33
commit b170b6ec8b
9 changed files with 277 additions and 225 deletions

View File

@@ -0,0 +1,183 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
var template = `
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()"></span>
</div>
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a>
<span>(</span><span class="query-part-parameters"></span><span>)</span>
`;
/** @ngInject */
export function queryPartEditorDirective($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
return {
restrict: 'E',
template: template,
scope: {
part: "=",
removeAction: "&",
partUpdated: "&",
getOptions: "&",
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
var $controlsContainer = elem.find('.tight-form-func-controls');
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(part.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 inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply($scope.partUpdated);
}
$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, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
var typeaheadSource = function (query, callback) {
if (param.options) { return param.options; }
$scope.$apply(function() {
$scope.getOptions().then(function(result) {
var dynamicOptions = _.map(result, function(op) { return op.value; });
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
var options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
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() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
}
$scope.toggleControls = function() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$controlsContainer.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$controlsContainer.show();
};
$scope.removeActionInternal = function() {
$scope.toggleControls();
$scope.removeAction();
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
var $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
}
};
}
coreModule.directive('queryPartEditor', queryPartEditorDirective);

View File

@@ -33,6 +33,7 @@ import {Emitter} from './utils/emitter';
import {layoutSelector} from './components/layout_selector/layout_selector';
import {switchDirective} from './components/switch';
import {dashboardSelector} from './components/dashboard_selector';
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
import 'app/core/controllers/all';
import 'app/core/services/all';
import 'app/core/routes/routes';
@@ -56,4 +57,5 @@ export {
Emitter,
appEvents,
dashboardSelector,
queryPartEditorDirective,
};

View File

@@ -35,13 +35,13 @@
</div>
<div class="gf-form" ng-repeat="part in selectParts">
<influx-query-part-editor
<query-part-editor
class="gf-form-label query-part"
part="part"
remove-action="ctrl.removeSelectPart(selectParts, part)"
part-updated="ctrl.selectPartUpdated(selectParts, part)"
get-options="ctrl.getPartOptions(part)">
</influx-query-part-editor>
</query-part-editor>
</div>
<div class="gf-form">
@@ -62,12 +62,12 @@
<span>GROUP BY</span>
</label>
<influx-query-part-editor
<query-part-editor
ng-repeat="part in ctrl.queryModel.groupByParts"
part="part"
class="gf-form-label query-part"
remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)">
</influx-query-part-editor>
</query-part-editor>
</div>
<div class="gf-form">

View File

@@ -1,5 +0,0 @@
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
</div>
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>

View File

@@ -1,8 +1,5 @@
///<reference path="../../../headers/common.d.ts" />
import './query_part_editor';
import './query_part_editor';
import angular from 'angular';
import _ from 'lodash';
import InfluxQueryBuilder from './query_builder';

View File

@@ -1,178 +0,0 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('influxQueryPartEditor', function($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
return {
restrict: 'E',
templateUrl: 'public/app/plugins/datasource/influxdb/partials/query_part.html',
scope: {
part: "=",
removeAction: "&",
partUpdated: "&",
getOptions: "&",
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
var $controlsContainer = elem.find('.tight-form-func-controls');
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(part.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 inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply($scope.partUpdated);
}
$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, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
var typeaheadSource = function (query, callback) {
if (param.options) { return param.options; }
$scope.$apply(function() {
$scope.getOptions().then(function(result) {
var dynamicOptions = _.map(result, function(op) { return op.value; });
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
var options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
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() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
}
$scope.toggleControls = function() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$controlsContainer.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$controlsContainer.show();
};
$scope.removeActionInternal = function() {
$scope.toggleControls();
$scope.removeAction();
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
var $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
}
};
});
});

View File

@@ -1,18 +1,34 @@
<query-editor-row query-ctrl="ctrl" can-collapse="false">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-8">Query</label>
<input type="text" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck='false' placeholder="query expression" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()">
<div class="gf-form">
<label class="gf-form-label width-8 query-keyword">Query</label>
<metric-segment segment="ctrl.metricSegment" get-options="ctrl.getMetricOptions()" on-change="ctrl.queryChanged()"></metric-segment>
</div>
<div class="gf-form max-width-22">
<label class="gf-form-label">Metric lookup</label>
<input type="text" class="gf-form-input" ng-model="ctrl.target.metric" spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100>
<div class="gf-form" ng-repeat="part in ctrl.query.functions">
<query-part-editor class="gf-form-label query-part"
part="part"
remove-action="ctrl.removePart(query.functions, part)"
part-updated="ctrl.partUpdated(query.functions, part)"
get-options="ctrl.getPartOptions(part)">
</query-part-editor>
</div>
<div class="gf-form">
<label class="dropdown"
dropdown-typeahead="ctrl.addQueryPartMenu"
dropdown-typeahead-on-select="ctrl.addQueryPart($item, $subItem)">
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-26">
<label class="gf-form-label width-8">Legend format</label>
<label class="gf-form-label width-8 query-keyword">Legend format</label>
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat"
spellcheck='false' placeholder="legend format" data-min-length=0 data-items=1000
ng-model-onblur ng-change="ctrl.refreshMetricData()">

View File

@@ -7,6 +7,8 @@ import {
quotedIdentityRenderer,
} from 'app/core/components/query_part/query_part';
import _ from 'lodash';
var index = [];
var categories = {
Functions: [],
@@ -23,8 +25,21 @@ export class PromQuery {
constructor(target, templateSrv?, scopedVars?) {
this.target = target;
this.target.expr = this.target.expr || '';
this.target.intervalFactor = this.target.intervalFactor || 2;
this.target.functions = this.target.functions || [];
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
this.updateProjection();
}
updateProjection() {
this.functions = _.map(this.target.functions, function(func: any) {
return createPart(func);
});
}
render() {
@@ -33,18 +48,17 @@ export class PromQuery {
query += '[' + this.target.range + ']';
}
for (let funcModel of this.target.functions) {
var partDef = index[funcModel.type];
if (!partDef) {
continue;
}
var part = new QueryPart(funcModel, partDef);
query = part.render(query);
for (let func of this.functions) {
query = func.render(query);
}
return query;
}
addQueryPart(category, item) {
var partModel = createPart({type: item.text});
partModel.def.addStrategy(this, partModel);
}
}
export function createPart(part): any {
@@ -63,6 +77,7 @@ function register(options: any) {
function addFunctionStrategy(model, partModel) {
model.functions.push(partModel);
model.target.functions.push(partModel.part);
}
register({
@@ -74,6 +89,6 @@ register({
renderer: functionRenderer,
});
export function getCategories() {
export function getQueryPartCategories() {
return categories;
}

View File

@@ -6,46 +6,68 @@ import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
import {QueryCtrl} from 'app/plugins/sdk';
import {PromQuery, getQueryPartCategories} from './prom_query';
class PrometheusQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
metric: any;
query: any;
metricSegment: any;
addQueryPartMenu: any[];
resolutions: any;
oldTarget: any;
suggestMetrics: any;
linkToPrometheus: any;
/** @ngInject */
constructor($scope, $injector, private templateSrv) {
constructor($scope, $injector, private templateSrv, private uiSegmentSrv) {
super($scope, $injector);
var target = this.target;
target.expr = target.expr || '';
target.intervalFactor = target.intervalFactor || 2;
this.query = new PromQuery(this.target, templateSrv);
if (this.target.metric) {
this.metricSegment = uiSegmentSrv.newSegment(this.target.metric);
} else {
this.metricSegment = uiSegmentSrv.newSegment({value: 'select metric', fake: true});
}
this.metric = '';
this.resolutions = _.map([1,2,3,4,5,10], function(f) {
return {factor: f, label: '1/' + f};
});
$scope.$on('typeahead-updated', () => {
this.$scope.$apply(() => {
this.updateLink();
this.buildQueryPartMenu();
}
this.target.expr += this.target.metric;
this.metric = '';
this.refreshMetricData();
buildQueryPartMenu() {
var categories = getQueryPartCategories();
this.addQueryPartMenu = _.reduce(categories, function(memo, cat, key) {
var menu = {
text: key,
submenu: cat.map(item => {
return {text: item.type, value: item.type};
}),
};
memo.push(menu);
return memo;
}, []);
}
addQueryPart(item, subItem) {
this.query.addQueryPart(item, subItem);
this.panelCtrl.refresh();
}
getMetricOptions() {
return this.datasource.performSuggestQuery('').then(res => {
return _.map(res, metric => {
return this.uiSegmentSrv.newSegment(metric);
});
});
}
// called from typeahead so need this
// here in order to ensure this ref
this.suggestMetrics = (query, callback) => {
console.log(this);
this.datasource.performSuggestQuery(query).then(callback);
};
this.updateLink();
queryChanged() {
this.target.metric = this.metricSegment.value;
}
refreshMetricData() {