mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(query part): moved query part editor from influxdb to core
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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()">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user