From d3bbc245c9fb8d8dd7d0e28005490c928663cf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 14:32:25 +0200 Subject: [PATCH 1/5] feat(query_part): began query part refactor to be able to reuse it in prometheus query editor --- .../core/components/query_part/query_part.ts | 123 +++++++++++ .../query_part/query_part_editor.ts | 0 .../plugins/datasource/influxdb/query_part.ts | 198 +++++------------- 3 files changed, 171 insertions(+), 150 deletions(-) create mode 100644 public/app/core/components/query_part/query_part.ts create mode 100644 public/app/core/components/query_part/query_part_editor.ts diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts new file mode 100644 index 00000000000..90724f65d2d --- /dev/null +++ b/public/app/core/components/query_part/query_part.ts @@ -0,0 +1,123 @@ +/// + +import _ from 'lodash'; + +export class QueryPartDef { + type: string; + params: any[]; + defaultParams: any[]; + renderer: any; + category: any; + addStrategy: any; + + constructor(options: any) { + this.type = options.type; + this.params = options.params; + this.defaultParams = options.defaultParams; + this.renderer = options.renderer; + this.category = options.category; + this.addStrategy = options.addStrategy; + } +} + +export class QueryPart { + part: any; + def: QueryPartDef; + params: any[]; + text: string; + + constructor(part: any, def: any) { + this.part = part; + this.def = def; + if (!this.def) { + throw {message: 'Could not find query part ' + part.type}; + } + + part.params = part.params || _.clone(this.def.defaultParams); + this.params = part.params; + this.updateText(); + } + + render(innerExpr: string) { + return this.def.renderer(this, innerExpr); + } + + hasMultipleParamsInString (strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + } + + updateParam (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: string, 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.part.params = this.params; + this.updateText(); + } + + updateText() { + if (this.params.length === 0) { + this.text = this.def.type + '()'; + return; + } + + var text = this.def.type + '('; + text += this.params.join(', '); + text += ')'; + this.text = text; + } +} + +export function functionRenderer(part, innerExpr) { + var str = part.def.type + '('; + var parameters = _.map(part.params, (value, index) => { + var paramType = part.def.params[index]; + if (paramType.type === 'time') { + if (value === 'auto') { + value = '$interval'; + } + } + if (paramType.quote === 'single') { + return "'" + value + "'"; + } else if (paramType.quote === 'double') { + return '"' + value + '"'; + } + + return value; + }); + + if (innerExpr) { + parameters.unshift(innerExpr); + } + return str + parameters.join(', ') + ')'; +} + + +export function suffixRenderer(part, innerExpr) { + return innerExpr + ' ' + part.params[0]; +} + +export function identityRenderer(part, innerExpr) { + return part.params[0]; +} + +export function quotedIdentityRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} + + diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 63d70be5653..f22713a8057 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -1,6 +1,14 @@ /// import _ from 'lodash'; +import { + QueryPartDef, + QueryPart, + functionRenderer, + suffixRenderer, + identityRenderer, + quotedIdentityRenderer, +} from 'app/core/components/query_part/query_part'; var index = []; var categories = { @@ -12,71 +20,26 @@ var categories = { Fields: [], }; +function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw {message: 'Could not find query part ' + part.type}; + } + + return new QueryPart(part, def); +}; + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + var groupByTimeFunctions = []; -class QueryPartDef { - type: string; - params: any[]; - defaultParams: any[]; - renderer: any; - category: any; - addStrategy: any; - - constructor(options: any) { - this.type = options.type; - this.params = options.params; - this.defaultParams = options.defaultParams; - this.renderer = options.renderer; - this.category = options.category; - this.addStrategy = options.addStrategy; - } - - static register(options: any) { - index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); - } -} - -function functionRenderer(part, innerExpr) { - var str = part.def.type + '('; - var parameters = _.map(part.params, (value, index) => { - var paramType = part.def.params[index]; - if (paramType.type === 'time') { - if (value === 'auto') { - value = '$interval'; - } - } - if (paramType.quote === 'single') { - return "'" + value + "'"; - } else if (paramType.quote === 'double') { - return '"' + value + '"'; - } - - return value; - }); - - if (innerExpr) { - parameters.unshift(innerExpr); - } - return str + parameters.join(', ') + ')'; -} - function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } -function suffixRenderer(part, innerExpr) { - return innerExpr + ' ' + part.params[0]; -} - -function identityRenderer(part, innerExpr) { - return part.params[0]; -} - -function quotedIdentityRenderer(part, innerExpr) { - return '"' + part.params[0] + '"'; -} - function fieldRenderer(part, innerExpr) { if (part.params[0] === '*') { return '*'; @@ -149,13 +112,13 @@ function addAliasStrategy(selectParts, partModel) { function addFieldStrategy(selectParts, partModel, query) { // copy all parts var parts = _.map(selectParts, function(part: any) { - return new QueryPart({type: part.def.type, params: _.clone(part.params)}); + return createPart({type: part.def.type, params: _.clone(part.params)}); }); query.selectModels.push(parts); } -QueryPartDef.register({ +register({ type: 'field', addStrategy: addFieldStrategy, category: categories.Fields, @@ -165,7 +128,7 @@ QueryPartDef.register({ }); // Aggregations -QueryPartDef.register({ +register({ type: 'count', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -174,7 +137,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'distinct', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -183,7 +146,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'integral', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -192,7 +155,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'mean', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -201,7 +164,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'median', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -210,7 +173,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'sum', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -221,7 +184,7 @@ QueryPartDef.register({ // transformations -QueryPartDef.register({ +register({ type: 'derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -230,7 +193,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'non_negative_derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -239,7 +202,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'difference', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -248,7 +211,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'moving_average', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -257,7 +220,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'stddev', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -266,7 +229,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'time', category: groupByTimeFunctions, params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }], @@ -274,7 +237,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'fill', category: groupByTimeFunctions, params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }], @@ -283,7 +246,7 @@ QueryPartDef.register({ }); // Selectors -QueryPartDef.register({ +register({ type: 'bottom', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -292,7 +255,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'first', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -301,7 +264,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'last', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -310,7 +273,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'max', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -319,7 +282,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'min', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -328,7 +291,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'percentile', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -337,7 +300,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'top', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -346,7 +309,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'tag', category: groupByTimeFunctions, params: [{name: 'tag', type: 'string', dynamicLookup: true}], @@ -354,7 +317,7 @@ QueryPartDef.register({ renderer: fieldRenderer, }); -QueryPartDef.register({ +register({ type: 'math', addStrategy: addMathStrategy, category: categories.Math, @@ -363,7 +326,7 @@ QueryPartDef.register({ renderer: suffixRenderer, }); -QueryPartDef.register({ +register({ type: 'alias', addStrategy: addAliasStrategy, category: categories.Aliasing, @@ -373,74 +336,9 @@ QueryPartDef.register({ renderer: aliasRenderer, }); -class QueryPart { - part: any; - def: QueryPartDef; - params: any[]; - text: string; - - constructor(part: any) { - this.part = part; - this.def = index[part.type]; - if (!this.def) { - throw {message: 'Could not find query part ' + part.type}; - } - - part.params = part.params || _.clone(this.def.defaultParams); - this.params = part.params; - this.updateText(); - } - - render(innerExpr: string) { - return this.def.renderer(this, innerExpr); - } - - hasMultipleParamsInString (strValue, index) { - if (strValue.indexOf(',') === -1) { - return false; - } - - return this.def.params[index + 1] && this.def.params[index + 1].optional; - } - - updateParam (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: string, 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.part.params = this.params; - this.updateText(); - } - - updateText() { - if (this.params.length === 0) { - this.text = this.def.type + '()'; - return; - } - - var text = this.def.type + '('; - text += this.params.join(', '); - text += ')'; - this.text = text; - } -} export default { - create: function(part): any { - return new QueryPart(part); - }, - + create: createPart, getCategories: function() { return categories; } From cca37caf331ce62eb3cbdb16b02bbbeb1d9583c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 15:48:07 +0200 Subject: [PATCH 2/5] feat(prometheus): began work on prometheus query model --- .../datasource/prometheus/prom_query.ts | 79 +++++++++++++++++++ .../prometheus/specs/query_specs.ts | 23 ++++++ 2 files changed, 102 insertions(+) create mode 100644 public/app/plugins/datasource/prometheus/prom_query.ts create mode 100644 public/app/plugins/datasource/prometheus/specs/query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts new file mode 100644 index 00000000000..e9ef6ddf40d --- /dev/null +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -0,0 +1,79 @@ +import { + QueryPartDef, + QueryPart, + functionRenderer, + suffixRenderer, + identityRenderer, + quotedIdentityRenderer, +} from 'app/core/components/query_part/query_part'; + +var index = []; +var categories = { + Functions: [], +}; + +export class PromQuery { + target: any; + metric: string; + range: string; + filters: any[]; + functions: any[]; + templateSrv: any; + scopedVars: any; + + constructor(target, templateSrv?, scopedVars?) { + this.target = target; + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + } + + render() { + var query = this.target.metric; + if (this.target.range) { + 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); + } + + return query; + } +} + +export function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw {message: 'Could not find query part ' + part.type}; + } + + return new QueryPart(part, def); +} + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + +function addFunctionStrategy(model, partModel) { + model.functions.push(partModel); +} + +register({ + type: 'rate', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +export function getCategories() { + return categories; +} diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts new file mode 100644 index 00000000000..5bb164c306e --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/query_specs.ts @@ -0,0 +1,23 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {PromQuery} from '../prom_query'; + +describe.only('PromQuery', function() { + var templateSrv = {replace: val => val}; + + describe('render series with mesurement only', function() { + it('should generate correct query', function() { + var query = new PromQuery({ + metric: 'cpu', + range: '5m', + functions: [ + {type: 'rate', params: []} + ] + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('rate(cpu[5m])'); + }); + }); + +}); From b170b6ec8b91485370a3fc7f3f8c7b947a4a7cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 May 2016 14:52:44 +0200 Subject: [PATCH 3/5] feat(query part): moved query part editor from influxdb to core --- .../query_part/query_part_editor.ts | 183 ++++++++++++++++++ public/app/core/core.ts | 2 + .../influxdb/partials/query.editor.html | 8 +- .../influxdb/partials/query_part.html | 5 - .../plugins/datasource/influxdb/query_ctrl.ts | 3 - .../datasource/influxdb/query_part_editor.js | 178 ----------------- .../prometheus/partials/query.editor.html | 30 ++- .../datasource/prometheus/prom_query.ts | 33 +++- .../datasource/prometheus/query_ctrl.ts | 60 ++++-- 9 files changed, 277 insertions(+), 225 deletions(-) delete mode 100644 public/app/plugins/datasource/influxdb/partials/query_part.html delete mode 100644 public/app/plugins/datasource/influxdb/query_part_editor.js diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index e69de29bb2d..f9122ee283b 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -0,0 +1,183 @@ +/// + +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
+ +
+ +{{part.def.type}} +() +`; + + /** @ngInject */ +export function queryPartEditorDirective($compile, templateSrv) { + + var paramTemplate = ''; + 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) { + $(', ').appendTo($paramsContainer); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); + var $paramLink = $('' + paramValue + ''); + 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); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index abebb5ce560..9c8ae9cdad2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -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, }; diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0d6dd5c5c3b..68b8ee60d98 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -35,13 +35,13 @@
- - +
@@ -62,12 +62,12 @@ GROUP BY - - +
diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html deleted file mode 100644 index 478edfe5c29..00000000000 --- a/public/app/plugins/datasource/influxdb/partials/query_part.html +++ /dev/null @@ -1,5 +0,0 @@ -
- -
- -{{part.def.type}}() diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 8d6a03fc4a1..69895e53c25 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -1,8 +1,5 @@ /// -import './query_part_editor'; -import './query_part_editor'; - import angular from 'angular'; import _ from 'lodash'; import InfluxQueryBuilder from './query_builder'; diff --git a/public/app/plugins/datasource/influxdb/query_part_editor.js b/public/app/plugins/datasource/influxdb/query_part_editor.js deleted file mode 100644 index 4e044eca304..00000000000 --- a/public/app/plugins/datasource/influxdb/query_part_editor.js +++ /dev/null @@ -1,178 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', -], -function (angular, _, $) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('influxQueryPartEditor', function($compile, templateSrv) { - - var paramTemplate = ''; - 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) { - $(', ').appendTo($paramsContainer); - } - - var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); - var $paramLink = $('' + paramValue + ''); - 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(); - } - }; - - }); - -}); diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 37c27ace58a..a67e3e9b188 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,18 +1,34 @@
-
- - +
+ +
-
- - + +
+ + +
+ +
+ +
+ +
+
- + diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts index e9ef6ddf40d..83ac4f1615f 100644 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -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; } diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 8284268ef39..25a803b7bb3 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -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() { From c77d72b29fb806eb848987f085fa1eebf16e4818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 May 2016 18:03:17 +0200 Subject: [PATCH 4/5] feat(prometheus): progress on new prometheus query editor, #5117 --- .../prometheus/metric_find_query.js | 128 +++++++++++------- .../prometheus/partials/query.editor.html | 9 +- .../datasource/prometheus/prom_query.ts | 27 +++- .../datasource/prometheus/query_ctrl.ts | 42 +++++- .../specs/metric_find_query_specs.ts | 37 +++++ .../prometheus/specs/query_specs.ts | 17 ++- 6 files changed, 207 insertions(+), 53 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.js b/public/app/plugins/datasource/prometheus/metric_find_query.js index f449978be81..d3d5376bc19 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.js +++ b/public/app/plugins/datasource/prometheus/metric_find_query.js @@ -11,27 +11,33 @@ function (_) { } PrometheusMetricFindQuery.prototype.process = function() { - var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metric_names_regex = /^metrics\((.+)\)$/; - var query_result_regex = /^query_result\((.+)\)$/; + var labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; + var metricNamesRegex = /^metrics\((.+)\)$/; + var labelsRegex = /^labels\((.+)\)$/; + var queryResultRegex = /^query_result\((.+)\)$/; - var label_values_query = this.query.match(label_values_regex); - if (label_values_query) { - if (label_values_query[1]) { - return this.labelValuesQuery(label_values_query[2], label_values_query[1]); + var labelsQuery = this.query.match(labelsRegex); + if (labelsQuery) { + return this.labelsQuery(labelsQuery[1]); + } + + var labelValuesQuery = this.query.match(labelValuesRegex); + if (labelValuesQuery) { + if (labelValuesQuery[1]) { + return this.labelValuesQuery(labelValuesQuery[2], labelValuesQuery[1]); } else { - return this.labelValuesQuery(label_values_query[2], null); + return this.labelValuesQuery(labelValuesQuery[2], null); } } - var metric_names_query = this.query.match(metric_names_regex); - if (metric_names_query) { - return this.metricNameQuery(metric_names_query[1]); + var metricNamesQuery = this.query.match(metricNamesRegex); + if (metricNamesQuery) { + return this.metricNameQuery(metricNamesQuery[1]); } - var query_result_query = this.query.match(query_result_regex); - if (query_result_query) { - return this.queryResultQuery(query_result_query[1]); + var queryResultQuery = this.query.match(queryResultRegex); + if (queryResultQuery) { + return this.queryResultQuery(queryResultQuery[1]); } // if query contains full metric name, return metric name and label list @@ -67,45 +73,71 @@ function (_) { } }; + PrometheusMetricFindQuery.prototype.labelsQuery = function(metric) { + var url; + + url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + + '&start=' + (this.range.from.valueOf() / 1000) + + '&end=' + (this.range.to.valueOf() / 1000); + + return this.datasource._request('GET', url) + .then(function(result) { + var tags = {}; + _.each(result.data.data, function(metric) { + _.each(metric, function(value, key) { + if (key === "__name__") { + return; + } + + tags[key] = key; + }); + }); + + return _.map(tags, function(value) { + return {text: value, value: value}; + }); + }); + }; + PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; return this.datasource._request('GET', url) - .then(function(result) { - return _.chain(result.data.data) - .filter(function(metricName) { - var r = new RegExp(metricFilterPattern); - return r.test(metricName); - }) - .map(function(matchedMetricName) { - return { - text: matchedMetricName, - expandable: true - }; - }) - .value(); - }); + .then(function(result) { + return _.chain(result.data.data) + .filter(function(metricName) { + var r = new RegExp(metricFilterPattern); + return r.test(metricName); + }) + .map(function(matchedMetricName) { + return { + text: matchedMetricName, + expandable: true + }; + }) + .value(); + }); }; PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) { var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000); return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data.result, function(metricData) { - var text = metricData.metric.__name__ || ''; - delete metricData.metric.__name__; - text += '{' + - _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + - '}'; - text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; + .then(function(result) { + return _.map(result.data.data.result, function(metricData) { + var text = metricData.metric.__name__ || ''; + delete metricData.metric.__name__; + text += '{' + + _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + + '}'; + text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; - return { - text: text, - expandable: true - }; + return { + text: text, + expandable: true + }; + }); }); - }); }; PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) { @@ -115,14 +147,14 @@ function (_) { var self = this; return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data, function(metric) { - return { - text: self.datasource.getOriginalMetricName(metric), - expandable: true - }; + .then(function(result) { + return _.map(result.data.data, function(metric) { + return { + text: self.datasource.getOriginalMetricName(metric), + expandable: true + }; + }); }); - }); }; return PrometheusMetricFindQuery; diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index a67e3e9b188..359c366cb0f 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,5 +1,10 @@ - -
+ + +
+ +
+ +
diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts index 83ac4f1615f..d73a8b1cbcf 100644 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -2,7 +2,6 @@ import { QueryPartDef, QueryPart, functionRenderer, - suffixRenderer, identityRenderer, quotedIdentityRenderer, } from 'app/core/components/query_part/query_part'; @@ -12,6 +11,7 @@ import _ from 'lodash'; var index = []; var categories = { Functions: [], + GroupBy: [], }; export class PromQuery { @@ -29,6 +29,7 @@ export class PromQuery { this.target.expr = this.target.expr || ''; this.target.intervalFactor = this.target.intervalFactor || 2; this.target.functions = this.target.functions || []; + this.target.editorMode = this.target.editorMode || true; this.templateSrv = templateSrv; this.scopedVars = scopedVars; @@ -80,6 +81,10 @@ function addFunctionStrategy(model, partModel) { model.target.functions.push(partModel.part); } +function groupByLabelRenderer(part, innerExpr) { + return innerExpr + ' by(' + part.params.join(',') + ')'; +} + register({ type: 'rate', addStrategy: addFunctionStrategy, @@ -89,6 +94,26 @@ register({ renderer: functionRenderer, }); +register({ + type: 'sum', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'by', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [ + {name: "label", type: "string", dynamicLookup: true} + ], + defaultParams: [], + renderer: groupByLabelRenderer, +}); + export function getQueryPartCategories() { return categories; } diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 8692d915d30..680b1294df6 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -20,7 +20,7 @@ class PrometheusQueryCtrl extends QueryCtrl { linkToPrometheus: any; /** @ngInject */ - constructor($scope, $injector, private templateSrv, private uiSegmentSrv) { + constructor($scope, $injector, private templateSrv, private uiSegmentSrv, private $rootScope) { super($scope, $injector); this.query = new PromQuery(this.target, templateSrv); @@ -58,6 +58,39 @@ class PrometheusQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } + getPartOptions(part) { + if (part.def.type === 'by') { + return this.datasource.metricFindQuery('labels(' + this.target.metric + ')') + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(true)); + } + } + + partUpdated(part) { + this.target.expr = this.query.render(); + this.panelCtrl.refresh(); + } + + handleQueryError(err) { + this.$rootScope.appEvent('alert-error', ['Query failed', err.message]); + } + + transformToSegments(addTemplateVars) { + return (results) => { + var segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable }); + }); + + if (addTemplateVars) { + for (let variable of this.templateSrv.variables) { + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); + } + } + + return segments; + }; + } + getMetricOptions() { return this.datasource.performSuggestQuery('').then(res => { return _.map(res, metric => { @@ -68,6 +101,13 @@ class PrometheusQueryCtrl extends QueryCtrl { queryChanged() { this.target.metric = this.metricSegment.value; + this.target.expr = this.query.render(); + this.refresh(); + } + + toggleEditorMode() { + this.target.expr = this.query.render(false); + this.target.editorMode = !this.target.editorMode; } refreshMetricData() { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index 38b50d2ce79..b393f36402c 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -22,18 +22,22 @@ describe('PrometheusMetricFindQuery', function() { describe('When performing metricFindQuery', function() { var results; var response; + it('label_values(resource) should generate label search query', function() { response = { status: "success", data: ["value1", "value2", "value3"] }; + ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('label_values(metric, resource) should generate series query', function() { response = { status: "success", @@ -43,13 +47,16 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('label_values(metric, resource) should pass correct time', function() { ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') }); ctx.$httpBackend.expect('GET', @@ -59,6 +66,7 @@ describe('PrometheusMetricFindQuery', function() { ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); }); + it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { response = { status: "success", @@ -68,6 +76,7 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -75,18 +84,22 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); + it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", data: ["metric1","metric2","metric3","nomatch"] }; + ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('query_result(metric) should generate metric name query', function() { response = { status: "success", @@ -98,6 +111,7 @@ describe('PrometheusMetricFindQuery', function() { }] } }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -106,5 +120,28 @@ describe('PrometheusMetricFindQuery', function() { expect(results.length).to.be(1); expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); }); + + it('labels(metric) should generate series query', function() { + response = { + status: "success", + data: [ + {__name__: "metric", resource: "value1", app: "app1"}, + {__name__: "metric", resource: "value2", app: "app2"}, + {__name__: "metric", resource: "value3", server: "server1"} + ] + }; + + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); + var pm = new PrometheusMetricFindQuery(ctx.ds, 'labels(metric)', ctx.timeSrv); + pm.process().then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + ctx.$rootScope.$apply(); + + expect(results.length).to.be(3); + expect(results[0].text).to.be('resource'); + expect(results[1].text).to.be('app'); + expect(results[2].text).to.be('server'); + }); + }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts index 5bb164c306e..d58e0834f89 100644 --- a/public/app/plugins/datasource/prometheus/specs/query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/query_specs.ts @@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; import {PromQuery} from '../prom_query'; -describe.only('PromQuery', function() { +describe('PromQuery', function() { var templateSrv = {replace: val => val}; describe('render series with mesurement only', function() { @@ -20,4 +20,19 @@ describe.only('PromQuery', function() { }); }); + describe('render series with group by label', function() { + it('should generate correct query', function() { + var query = new PromQuery({ + metric: 'cpu', + functions: [ + {type: 'sum', params: []}, + {type: 'by', params: ['app']}, + ] + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('sum(cpu) by(app)'); + }); + }); + }); From b8aa8b307997dde9f11c28401436cc841f087726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 9 Jun 2016 10:37:33 +0200 Subject: [PATCH 5/5] feat(prometheus): restore old prometheus query editor, revert this commit in prometheus query editor v2 branch --- .../datasource/prometheus/datasource.ts | 25 ++-- .../prometheus/metric_find_query.js | 128 +++++++----------- .../prometheus/partials/query.editor.html | 39 ++---- .../datasource/prometheus/prom_query.ts | 119 ---------------- .../datasource/prometheus/query_ctrl.ts | 102 +++----------- .../specs/metric_find_query_specs.ts | 37 ----- .../prometheus/specs/query_specs.ts | 38 ------ 7 files changed, 87 insertions(+), 401 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/prom_query.ts delete mode 100644 public/app/plugins/datasource/prometheus/specs/query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index f1c58f6a61d..cbd7b1ba24b 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -256,23 +256,14 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS return this.renderTemplate(options.legendFormat, labelData) || '{}'; }; - this.renderTemplate = function(format, data) { - var originalSettings = _.templateSettings; - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - - var template = _.template(templateSrv.replace(format)); - var result; - try { - result = template(data); - } catch (e) { - result = null; - } - - _.templateSettings = originalSettings; - - return result; + this.renderTemplate = function(aliasPattern, aliasData) { + var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; + return aliasPattern.replace(aliasRegex, function(match, g1) { + if (aliasData[g1]) { + return aliasData[g1]; + } + return g1; + }); }; this.getOriginalMetricName = function(labelData) { diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.js b/public/app/plugins/datasource/prometheus/metric_find_query.js index d3d5376bc19..f449978be81 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.js +++ b/public/app/plugins/datasource/prometheus/metric_find_query.js @@ -11,33 +11,27 @@ function (_) { } PrometheusMetricFindQuery.prototype.process = function() { - var labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metricNamesRegex = /^metrics\((.+)\)$/; - var labelsRegex = /^labels\((.+)\)$/; - var queryResultRegex = /^query_result\((.+)\)$/; + var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; + var metric_names_regex = /^metrics\((.+)\)$/; + var query_result_regex = /^query_result\((.+)\)$/; - var labelsQuery = this.query.match(labelsRegex); - if (labelsQuery) { - return this.labelsQuery(labelsQuery[1]); - } - - var labelValuesQuery = this.query.match(labelValuesRegex); - if (labelValuesQuery) { - if (labelValuesQuery[1]) { - return this.labelValuesQuery(labelValuesQuery[2], labelValuesQuery[1]); + var label_values_query = this.query.match(label_values_regex); + if (label_values_query) { + if (label_values_query[1]) { + return this.labelValuesQuery(label_values_query[2], label_values_query[1]); } else { - return this.labelValuesQuery(labelValuesQuery[2], null); + return this.labelValuesQuery(label_values_query[2], null); } } - var metricNamesQuery = this.query.match(metricNamesRegex); - if (metricNamesQuery) { - return this.metricNameQuery(metricNamesQuery[1]); + var metric_names_query = this.query.match(metric_names_regex); + if (metric_names_query) { + return this.metricNameQuery(metric_names_query[1]); } - var queryResultQuery = this.query.match(queryResultRegex); - if (queryResultQuery) { - return this.queryResultQuery(queryResultQuery[1]); + var query_result_query = this.query.match(query_result_regex); + if (query_result_query) { + return this.queryResultQuery(query_result_query[1]); } // if query contains full metric name, return metric name and label list @@ -73,71 +67,45 @@ function (_) { } }; - PrometheusMetricFindQuery.prototype.labelsQuery = function(metric) { - var url; - - url = '/api/v1/series?match[]=' + encodeURIComponent(metric) - + '&start=' + (this.range.from.valueOf() / 1000) - + '&end=' + (this.range.to.valueOf() / 1000); - - return this.datasource._request('GET', url) - .then(function(result) { - var tags = {}; - _.each(result.data.data, function(metric) { - _.each(metric, function(value, key) { - if (key === "__name__") { - return; - } - - tags[key] = key; - }); - }); - - return _.map(tags, function(value) { - return {text: value, value: value}; - }); - }); - }; - PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; return this.datasource._request('GET', url) - .then(function(result) { - return _.chain(result.data.data) - .filter(function(metricName) { - var r = new RegExp(metricFilterPattern); - return r.test(metricName); - }) - .map(function(matchedMetricName) { - return { - text: matchedMetricName, - expandable: true - }; - }) - .value(); - }); + .then(function(result) { + return _.chain(result.data.data) + .filter(function(metricName) { + var r = new RegExp(metricFilterPattern); + return r.test(metricName); + }) + .map(function(matchedMetricName) { + return { + text: matchedMetricName, + expandable: true + }; + }) + .value(); + }); }; PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) { var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000); return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data.result, function(metricData) { - var text = metricData.metric.__name__ || ''; - delete metricData.metric.__name__; - text += '{' + - _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + - '}'; - text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; + .then(function(result) { + return _.map(result.data.data.result, function(metricData) { + var text = metricData.metric.__name__ || ''; + delete metricData.metric.__name__; + text += '{' + + _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + + '}'; + text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; - return { - text: text, - expandable: true - }; - }); + return { + text: text, + expandable: true + }; }); + }); }; PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) { @@ -147,14 +115,14 @@ function (_) { var self = this; return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data, function(metric) { - return { - text: self.datasource.getOriginalMetricName(metric), - expandable: true - }; - }); + .then(function(result) { + return _.map(result.data.data, function(metric) { + return { + text: self.datasource.getOriginalMetricName(metric), + expandable: true + }; }); + }); }; return PrometheusMetricFindQuery; diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 359c366cb0f..37c27ace58a 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,39 +1,18 @@ - - -
- -
- -
-
- - -
- -
- - -
- -
- -
- + +
-
+ + +
+
+ +
- + diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts deleted file mode 100644 index d73a8b1cbcf..00000000000 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - QueryPartDef, - QueryPart, - functionRenderer, - identityRenderer, - quotedIdentityRenderer, -} from 'app/core/components/query_part/query_part'; - -import _ from 'lodash'; - -var index = []; -var categories = { - Functions: [], - GroupBy: [], -}; - -export class PromQuery { - target: any; - metric: string; - range: string; - filters: any[]; - functions: any[]; - templateSrv: any; - scopedVars: any; - - 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.target.editorMode = this.target.editorMode || true; - - this.templateSrv = templateSrv; - this.scopedVars = scopedVars; - - this.updateProjection(); - } - - updateProjection() { - this.functions = _.map(this.target.functions, function(func: any) { - return createPart(func); - }); - } - - render() { - var query = this.target.metric; - if (this.target.range) { - query += '[' + this.target.range + ']'; - } - - 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 { - var def = index[part.type]; - if (!def) { - throw {message: 'Could not find query part ' + part.type}; - } - - return new QueryPart(part, def); -} - -function register(options: any) { - index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); -} - -function addFunctionStrategy(model, partModel) { - model.functions.push(partModel); - model.target.functions.push(partModel.part); -} - -function groupByLabelRenderer(part, innerExpr) { - return innerExpr + ' by(' + part.params.join(',') + ')'; -} - -register({ - type: 'rate', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'sum', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'by', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [ - {name: "label", type: "string", dynamicLookup: true} - ], - defaultParams: [], - renderer: groupByLabelRenderer, -}); - -export function getQueryPartCategories() { - return categories; -} diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 680b1294df6..92cec328d87 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -6,108 +6,46 @@ 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'; - query: any; - metricSegment: any; - addQueryPartMenu: any[]; + metric: any; resolutions: any; oldTarget: any; suggestMetrics: any; linkToPrometheus: any; /** @ngInject */ - constructor($scope, $injector, private templateSrv, private uiSegmentSrv, private $rootScope) { + constructor($scope, $injector, private templateSrv) { super($scope, $injector); - 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}); - } + var target = this.target; + target.expr = target.expr || ''; + target.intervalFactor = target.intervalFactor || 2; + this.metric = ''; this.resolutions = _.map([1,2,3,4,5,10], function(f) { return {factor: f, label: '1/' + f}; }); - this.updateLink(); - this.buildQueryPartMenu(); - } + $scope.$on('typeahead-updated', () => { + this.$scope.$apply(() => { - 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(); - } - - getPartOptions(part) { - if (part.def.type === 'by') { - return this.datasource.metricFindQuery('labels(' + this.target.metric + ')') - .then(this.transformToSegments(true)) - .catch(this.handleQueryError.bind(true)); - } - } - - partUpdated(part) { - this.target.expr = this.query.render(); - this.panelCtrl.refresh(); - } - - handleQueryError(err) { - this.$rootScope.appEvent('alert-error', ['Query failed', err.message]); - } - - transformToSegments(addTemplateVars) { - return (results) => { - var segments = _.map(results, segment => { - return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable }); - }); - - if (addTemplateVars) { - for (let variable of this.templateSrv.variables) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); - } - } - - return segments; - }; - } - - getMetricOptions() { - return this.datasource.performSuggestQuery('').then(res => { - return _.map(res, metric => { - return this.uiSegmentSrv.newSegment(metric); + this.target.expr += this.target.metric; + this.metric = ''; + this.refreshMetricData(); }); }); - } - queryChanged() { - this.target.metric = this.metricSegment.value; - this.target.expr = this.query.render(); - this.refresh(); - } + // 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); + }; - toggleEditorMode() { - this.target.expr = this.query.render(false); - this.target.editorMode = !this.target.editorMode; + this.updateLink(); } refreshMetricData() { @@ -120,6 +58,10 @@ class PrometheusQueryCtrl extends QueryCtrl { updateLink() { var range = this.panelCtrl.range; + if (!range) { + return; + } + var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000); var endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); var expr = { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index b393f36402c..38b50d2ce79 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -22,22 +22,18 @@ describe('PrometheusMetricFindQuery', function() { describe('When performing metricFindQuery', function() { var results; var response; - it('label_values(resource) should generate label search query', function() { response = { status: "success", data: ["value1", "value2", "value3"] }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('label_values(metric, resource) should generate series query', function() { response = { status: "success", @@ -47,16 +43,13 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('label_values(metric, resource) should pass correct time', function() { ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') }); ctx.$httpBackend.expect('GET', @@ -66,7 +59,6 @@ describe('PrometheusMetricFindQuery', function() { ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); }); - it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { response = { status: "success", @@ -76,7 +68,6 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -84,22 +75,18 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); - it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", data: ["metric1","metric2","metric3","nomatch"] }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('query_result(metric) should generate metric name query', function() { response = { status: "success", @@ -111,7 +98,6 @@ describe('PrometheusMetricFindQuery', function() { }] } }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -120,28 +106,5 @@ describe('PrometheusMetricFindQuery', function() { expect(results.length).to.be(1); expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); }); - - it('labels(metric) should generate series query', function() { - response = { - status: "success", - data: [ - {__name__: "metric", resource: "value1", app: "app1"}, - {__name__: "metric", resource: "value2", app: "app2"}, - {__name__: "metric", resource: "value3", server: "server1"} - ] - }; - - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'labels(metric)', ctx.timeSrv); - pm.process().then(function(data) { results = data; }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - - expect(results.length).to.be(3); - expect(results[0].text).to.be('resource'); - expect(results[1].text).to.be('app'); - expect(results[2].text).to.be('server'); - }); - }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts deleted file mode 100644 index d58e0834f89..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/query_specs.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; - -import {PromQuery} from '../prom_query'; - -describe('PromQuery', function() { - var templateSrv = {replace: val => val}; - - describe('render series with mesurement only', function() { - it('should generate correct query', function() { - var query = new PromQuery({ - metric: 'cpu', - range: '5m', - functions: [ - {type: 'rate', params: []} - ] - }, templateSrv, {}); - - var queryText = query.render(); - expect(queryText).to.be('rate(cpu[5m])'); - }); - }); - - describe('render series with group by label', function() { - it('should generate correct query', function() { - var query = new PromQuery({ - metric: 'cpu', - functions: [ - {type: 'sum', params: []}, - {type: 'by', params: ['app']}, - ] - }, templateSrv, {}); - - var queryText = query.render(); - expect(queryText).to.be('sum(cpu) by(app)'); - }); - }); - -});