-
-
diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html
new file mode 100644
index 00000000000..0eb0146ec13
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/partials/query_part.html
@@ -0,0 +1,5 @@
+
+
+
+
+
{{part.def.type}}()
diff --git a/public/app/plugins/datasource/influxdb/query_builder.js b/public/app/plugins/datasource/influxdb/query_builder.js
index 9ab0953b576..523003bd70d 100644
--- a/public/app/plugins/datasource/influxdb/query_builder.js
+++ b/public/app/plugins/datasource/influxdb/query_builder.js
@@ -4,8 +4,9 @@ define([
function (_) {
'use strict';
- function InfluxQueryBuilder(target) {
+ function InfluxQueryBuilder(target, queryModel) {
this.target = target;
+ this.model = queryModel;
if (target.groupByTags) {
target.groupBy = [{type: 'time', interval: 'auto'}];
@@ -92,78 +93,5 @@ function (_) {
return query;
};
- p._getGroupByTimeInterval = function(interval) {
- if (interval === 'auto') {
- return '$interval';
- }
- return interval;
- };
-
- p._buildQuery = function() {
- var target = this.target;
-
- if (!target.measurement) {
- throw "Metric measurement is missing";
- }
-
- if (!target.fields) {
- target.fields = [{name: 'value', func: target.function || 'mean'}];
- }
-
- var query = 'SELECT ';
- var i;
- for (i = 0; i < target.fields.length; i++) {
- var field = target.fields[i];
- if (i > 0) {
- query += ', ';
- }
- query += field.func + '("' + field.name + '")';
- if (field.mathExpr) {
- query += field.mathExpr;
- }
- if (field.asExpr) {
- query += ' AS "' + field.asExpr + '"';
- } else {
- query += ' AS "' + field.name + '"';
- }
- }
-
- var measurement = target.measurement;
- if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
- measurement = '"' + measurement+ '"';
- }
-
- query += ' FROM ' + measurement + ' WHERE ';
- var conditions = _.map(target.tags, function(tag, index) {
- return renderTagCondition(tag, index);
- });
-
- query += conditions.join(' ');
- query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
-
- query += ' GROUP BY';
- for (i = 0; i < target.groupBy.length; i++) {
- var group = target.groupBy[i];
- if (group.type === 'time') {
- query += ' time(' + this._getGroupByTimeInterval(group.interval) + ')';
- } else {
- query += ', "' + group.key + '"';
- }
- }
-
- if (target.fill) {
- query += ' fill(' + target.fill + ')';
- }
-
- target.query = query;
-
- return query;
- };
-
- p._modifyRawQuery = function () {
- var query = this.target.query.replace(";", "");
- return query;
- };
-
return InfluxQueryBuilder;
});
diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.js b/public/app/plugins/datasource/influxdb/query_ctrl.js
index 75dd972fbaa..38f87ecd84e 100644
--- a/public/app/plugins/datasource/influxdb/query_ctrl.js
+++ b/public/app/plugins/datasource/influxdb/query_ctrl.js
@@ -2,32 +2,33 @@ define([
'angular',
'lodash',
'./query_builder',
+ './influx_query',
+ './query_part',
+ './query_part_editor',
],
-function (angular, _, InfluxQueryBuilder) {
+function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
'use strict';
var module = angular.module('grafana.controllers');
- module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q, uiSegmentSrv) {
+ module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
$scope.init = function() {
if (!$scope.target) { return; }
- var target = $scope.target;
- target.tags = target.tags || [];
- target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
- target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
+ $scope.target = $scope.target;
+ $scope.queryModel = new InfluxQuery($scope.target);
+ $scope.queryBuilder = new InfluxQueryBuilder($scope.target);
+ $scope.groupBySegment = uiSegmentSrv.newPlusButton();
- $scope.queryBuilder = new InfluxQueryBuilder(target);
-
- if (!target.measurement) {
+ if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
- $scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
+ $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
}
$scope.tagSegments = [];
- _.each(target.tags, function(tag) {
+ _.each($scope.target.tags, function(tag) {
if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~";
@@ -46,9 +47,69 @@ function (angular, _, InfluxQueryBuilder) {
});
$scope.fixTagSegments();
+ $scope.buildSelectMenu();
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
};
+ $scope.buildSelectMenu = function() {
+ var categories = queryPart.getCategories();
+ $scope.selectMenu = _.reduce(categories, function(memo, cat, key) {
+ var menu = {text: key};
+ menu.submenu = _.map(cat, function(item) {
+ return {text: item.type, value: item.type};
+ });
+ memo.push(menu);
+ return memo;
+ }, []);
+ };
+
+ $scope.getGroupByOptions = function() {
+ var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
+
+ return $scope.datasource.metricFindQuery(query)
+ .then(function(tags) {
+ var options = [];
+ if (!$scope.queryModel.hasFill()) {
+ options.push(uiSegmentSrv.newSegment({value: 'fill(null)'}));
+ }
+ if (!$scope.queryModel.hasGroupByTime()) {
+ options.push(uiSegmentSrv.newSegment({value: 'time($interval)'}));
+ }
+ _.each(tags, function(tag) {
+ options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
+ });
+ return options;
+ })
+ .then(null, $scope.handleQueryError);
+ };
+
+ $scope.groupByAction = function() {
+ $scope.queryModel.addGroupBy($scope.groupBySegment.value);
+ var plusButton = uiSegmentSrv.newPlusButton();
+ $scope.groupBySegment.value = plusButton.value;
+ $scope.groupBySegment.html = plusButton.html;
+ $scope.get_data();
+ };
+
+ $scope.removeGroupByPart = function(part, index) {
+ $scope.queryModel.removeGroupByPart(part, index);
+ $scope.get_data();
+ };
+
+ $scope.addSelectPart = function(selectParts, cat, subitem) {
+ $scope.queryModel.addSelectPart(selectParts, subitem.value);
+ $scope.get_data();
+ };
+
+ $scope.removeSelectPart = function(selectParts, part) {
+ $scope.queryModel.removeSelectPart(selectParts, part);
+ $scope.get_data();
+ };
+
+ $scope.selectPartUpdated = function() {
+ $scope.get_data();
+ };
+
$scope.fixTagSegments = function() {
var count = $scope.tagSegments.length;
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
@@ -58,38 +119,9 @@ function (angular, _, InfluxQueryBuilder) {
}
};
- $scope.addGroupBy = function() {
- $scope.target.groupBy.push({type: 'tag', key: "select tag"});
- };
-
- $scope.removeGroupBy = function(index) {
- $scope.target.groupBy.splice(index, 1);
- $scope.get_data();
- };
-
- $scope.addSelect = function() {
- $scope.target.fields.push({name: "select field", func: 'mean'});
- };
-
- $scope.removeSelect = function(index) {
- $scope.target.fields.splice(index, 1);
- $scope.get_data();
- };
-
- $scope.changeFunction = function(func) {
- $scope.target.function = func;
- $scope.$parent.get_data();
- };
-
$scope.measurementChanged = function() {
$scope.target.measurement = $scope.measurementSegment.value;
- $scope.$parent.get_data();
- };
-
- $scope.getFields = function() {
- var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
- return $scope.datasource.metricFindQuery(fieldsQuery)
- .then($scope.transformToSegments(false), $scope.handleQueryError);
+ $scope.get_data();
};
$scope.toggleQueryMode = function () {
@@ -102,20 +134,17 @@ function (angular, _, InfluxQueryBuilder) {
.then($scope.transformToSegments(true), $scope.handleQueryError);
};
- $scope.getFunctions = function () {
- var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
- 'stddev', 'first', 'last'
- ];
- return $q.when(_.map(functionList, function(func) {
- return uiSegmentSrv.newSegment(func);
- }));
- };
-
- $scope.getGroupByTimeIntervals = function () {
- var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d'];
- return $q.when(_.map(times, function(func) {
- return uiSegmentSrv.newSegment(func);
- }));
+ $scope.getPartOptions = function(part) {
+ if (part.def.type === 'field') {
+ var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
+ return $scope.datasource.metricFindQuery(fieldsQuery)
+ .then($scope.transformToSegments(true), $scope.handleQueryError);
+ }
+ if (part.def.type === 'tag') {
+ var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
+ return $scope.datasource.metricFindQuery(tagsQuery)
+ .then($scope.transformToSegments(true), $scope.handleQueryError);
+ }
};
$scope.handleQueryError = function(err) {
@@ -179,25 +208,8 @@ function (angular, _, InfluxQueryBuilder) {
.then(null, $scope.handleQueryError);
};
- $scope.addField = function() {
- $scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
- _.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
- };
-
- $scope.fieldChanged = function(field) {
- if (field.name === '-- remove from select --') {
- $scope.target.fields = _.without($scope.target.fields, field);
- }
- $scope.get_data();
- };
-
$scope.getTagOptions = function() {
- var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
-
- return $scope.datasource.metricFindQuery(query)
- .then($scope.transformToSegments(false))
- .then(null, $scope.handleQueryError);
- };
+ };
$scope.setFill = function(fill) {
$scope.target.fill = fill;
diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts
new file mode 100644
index 00000000000..56ea7eecd23
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/query_part.ts
@@ -0,0 +1,432 @@
+///
+
+import _ = require('lodash');
+
+var index = [];
+var categories = {
+ Aggregations: [],
+ Selectors: [],
+ Transformations: [],
+ Math: [],
+ Aliasing: [],
+ Fields: [],
+};
+
+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 '*';
+ }
+ return '"' + part.params[0] + '"';
+}
+
+function replaceAggregationAddStrategy(selectParts, partModel) {
+ // look for existing aggregation
+ for (var i = 0; i < selectParts.length; i++) {
+ var part = selectParts[i];
+ if (part.def.category === categories.Aggregations) {
+ selectParts[i] = partModel;
+ return;
+ }
+ if (part.def.category === categories.Selectors) {
+ selectParts[i] = partModel;
+ return;
+ }
+ }
+
+ selectParts.splice(1, 0, partModel);
+}
+
+function addTransformationStrategy(selectParts, partModel) {
+ var i;
+ // look for index to add transformation
+ for (i = 0; i < selectParts.length; i++) {
+ var part = selectParts[i];
+ if (part.def.category === categories.Math || part.def.category === categories.Aliasing) {
+ break;
+ }
+ }
+
+ selectParts.splice(i, 0, partModel);
+}
+
+function addMathStrategy(selectParts, partModel) {
+ var partCount = selectParts.length;
+ if (partCount > 0) {
+ // if last is math, replace it
+ if (selectParts[partCount-1].def.type === 'math') {
+ selectParts[partCount-1] = partModel;
+ return;
+ }
+ // if next to last is math, replace it
+ if (selectParts[partCount-2].def.type === 'math') {
+ selectParts[partCount-2] = partModel;
+ return;
+ }
+ // if last is alias add it before
+ else if (selectParts[partCount-1].def.type === 'alias') {
+ selectParts.splice(partCount-1, 0, partModel);
+ return;
+ }
+ }
+ selectParts.push(partModel);
+}
+
+function addAliasStrategy(selectParts, partModel) {
+ var partCount = selectParts.length;
+ if (partCount > 0) {
+ // if last is alias, replace it
+ if (selectParts[partCount-1].def.type === 'alias') {
+ selectParts[partCount-1] = partModel;
+ return;
+ }
+ }
+ selectParts.push(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)});
+ });
+
+ query.selectModels.push(parts);
+}
+
+QueryPartDef.register({
+ type: 'field',
+ addStrategy: addFieldStrategy,
+ category: categories.Fields,
+ params: [{type: 'field', dynamicLookup: true}],
+ defaultParams: ['value'],
+ renderer: fieldRenderer,
+});
+
+// Aggregations
+QueryPartDef.register({
+ type: 'count',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'distinct',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'integral',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'mean',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'median',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'sum',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Aggregations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+// transformations
+
+QueryPartDef.register({
+ type: 'derivative',
+ addStrategy: addTransformationStrategy,
+ category: categories.Transformations,
+ params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
+ defaultParams: ['10s'],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'non_negative_derivative',
+ addStrategy: addTransformationStrategy,
+ category: categories.Transformations,
+ params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
+ defaultParams: ['10s'],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'stddev',
+ addStrategy: addTransformationStrategy,
+ category: categories.Transformations,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'time',
+ category: groupByTimeFunctions,
+ params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }],
+ defaultParams: ['auto'],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'fill',
+ category: groupByTimeFunctions,
+ params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }],
+ defaultParams: ['null'],
+ renderer: functionRenderer,
+});
+
+// Selectors
+QueryPartDef.register({
+ type: 'bottom',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [{name: 'count', type: 'int'}],
+ defaultParams: [3],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'first',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'last',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'max',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'min',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [],
+ defaultParams: [],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'percentile',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [{name: 'nth', type: 'int'}],
+ defaultParams: [95],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'top',
+ addStrategy: replaceAggregationAddStrategy,
+ category: categories.Selectors,
+ params: [{name: 'count', type: 'int'}],
+ defaultParams: [3],
+ renderer: functionRenderer,
+});
+
+QueryPartDef.register({
+ type: 'tag',
+ category: groupByTimeFunctions,
+ params: [{name: 'tag', type: 'string', dynamicLookup: true}],
+ defaultParams: ['tag'],
+ renderer: fieldRenderer,
+});
+
+QueryPartDef.register({
+ type: 'math',
+ addStrategy: addMathStrategy,
+ category: categories.Math,
+ params: [{ name: "expr", type: "string"}],
+ defaultParams: [' / 100'],
+ renderer: suffixRenderer,
+});
+
+QueryPartDef.register({
+ type: 'alias',
+ addStrategy: addAliasStrategy,
+ category: categories.Aliasing,
+ params: [{ name: "name", type: "string", quote: 'double'}],
+ defaultParams: ['alias'],
+ renderMode: 'suffix',
+ 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 = {
+ create: function(part): any {
+ return new QueryPart(part);
+ },
+
+ getCategories: function() {
+ return categories;
+ }
+};
diff --git a/public/app/plugins/datasource/influxdb/query_part_editor.js b/public/app/plugins/datasource/influxdb/query_part_editor.js
new file mode 100644
index 00000000000..9d81e315989
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/query_part_editor.js
@@ -0,0 +1,178 @@
+define([
+ 'angular',
+ 'lodash',
+ 'jquery',
+],
+function (angular, _, $) {
+ 'use strict';
+
+ angular
+ .module('grafana.directives')
+ .directive('influxQueryPartEditor', function($compile, templateSrv) {
+
+ var paramTemplate = '
';
+ return {
+ restrict: 'E',
+ templateUrl: '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/influxdb/specs/influx_query_specs.ts b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
new file mode 100644
index 00000000000..887bdd9c9b6
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts
@@ -0,0 +1,216 @@
+import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+
+import InfluxQuery = require('../influx_query');
+
+describe('InfluxQuery', function() {
+
+ describe('render series with mesurement only', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)');
+ });
+ });
+
+ describe('render series with math and alias', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [
+ [
+ {type: 'field', params: ['value']},
+ {type: 'mean', params: []},
+ {type: 'math', params: ['/100']},
+ {type: 'alias', params: ['text']},
+ ]
+ ]
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)');
+ });
+ });
+
+ describe('series with single tag only', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: [{type: 'time', params: ['auto']}],
+ tags: [{key: 'hostname', value: 'server1'}]
+ });
+
+ var queryText = query.render();
+
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
+ + ' GROUP BY time($interval)');
+ });
+
+ it('should switch regex operator with tag value is regex', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: [{type: 'time', params: ['auto']}],
+ tags: [{key: 'app', value: '/e.*/'}]
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)');
+ });
+ });
+
+ describe('series with multiple tags only', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: [{type: 'time', params: ['auto']}],
+ tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
+ '$timeFilter GROUP BY time($interval)');
+ });
+ });
+
+ describe('series with tags OR condition', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: [{type: 'time', params: ['auto']}],
+ tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
+ '$timeFilter GROUP BY time($interval)');
+ });
+ });
+
+ describe('series with groupByTag', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ tags: [],
+ groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', params: ['host']}],
+ });
+
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter ' +
+ 'GROUP BY time($interval), "host"');
+ });
+ });
+
+ describe('render series without group by', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}]],
+ groupBy: [],
+ });
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter');
+ });
+ });
+
+ describe('render series without group by and fill', function() {
+ it('should generate correct query', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}]],
+ groupBy: [{type: 'time'}, {type: 'fill', params: ['0']}],
+ });
+ var queryText = query.render();
+ expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(0)');
+ });
+ });
+
+ describe('when adding group by part', function() {
+
+ it('should add tag before fill', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: [{type: 'time'}, {type: 'fill'}]
+ });
+
+ query.addGroupBy('tag(host)');
+ expect(query.target.groupBy.length).to.be(3);
+ expect(query.target.groupBy[1].type).to.be('tag');
+ expect(query.target.groupBy[1].params[0]).to.be('host');
+ expect(query.target.groupBy[2].type).to.be('fill');
+ });
+
+ it('should add tag last if no fill', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ groupBy: []
+ });
+
+ query.addGroupBy('tag(host)');
+ expect(query.target.groupBy.length).to.be(1);
+ expect(query.target.groupBy[0].type).to.be('tag');
+ });
+
+ });
+
+ describe('when adding select part', function() {
+
+ it('should add mean after after field', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}]]
+ });
+
+ query.addSelectPart(query.selectModels[0], 'mean');
+ expect(query.target.select[0].length).to.be(2);
+ expect(query.target.select[0][1].type).to.be('mean');
+ });
+
+ it('should replace sum by mean', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}, {type: 'mean'}]]
+ });
+
+ query.addSelectPart(query.selectModels[0], 'sum');
+ expect(query.target.select[0].length).to.be(2);
+ expect(query.target.select[0][1].type).to.be('sum');
+ });
+
+ it('should add math before alias', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'alias'}]]
+ });
+
+ query.addSelectPart(query.selectModels[0], 'math');
+ expect(query.target.select[0].length).to.be(4);
+ expect(query.target.select[0][2].type).to.be('math');
+ });
+
+ it('should add math last', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}, {type: 'mean'}]]
+ });
+
+ query.addSelectPart(query.selectModels[0], 'math');
+ expect(query.target.select[0].length).to.be(3);
+ expect(query.target.select[0][2].type).to.be('math');
+ });
+
+ it('should replace math', function() {
+ var query = new InfluxQuery({
+ measurement: 'cpu',
+ select: [[{type: 'field', params: ['value']}, {type: 'mean'}, {type: 'math'}]]
+ });
+
+ query.addSelectPart(query.selectModels[0], 'math');
+ expect(query.target.select[0].length).to.be(3);
+ expect(query.target.select[0][2].type).to.be('math');
+ });
+
+ });
+
+});
diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts
index 65a2f453385..69b7aa0b6cd 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts
@@ -6,116 +6,6 @@ declare var InfluxQueryBuilder: any;
describe('InfluxQueryBuilder', function() {
- describe('series with mesurement only', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- groupBy: [{type: 'time', interval: 'auto'}]
- });
-
- var query = builder.build();
-
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with math expr and as expr', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
- groupBy: [{type: 'time', interval: 'auto'}]
- });
-
- var query = builder.build();
-
- expect(query).to.be('SELECT max("test")*2 AS "new_name" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with single tag only', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- groupBy: [{type: 'time', interval: 'auto'}],
- tags: [{key: 'hostname', value: 'server1'}]
- });
-
- var query = builder.build();
-
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
- + ' GROUP BY time($interval)');
- });
-
- it('should switch regex operator with tag value is regex', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- groupBy: [{type: 'time', interval: 'auto'}],
- tags: [{key: 'app', value: '/e.*/'}]
- });
-
- var query = builder.build();
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with multiple fields', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- tags: [],
- groupBy: [{type: 'time', interval: 'auto'}],
- fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
- });
-
- var query = builder.build();
- expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' +
- 'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with multiple tags only', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- groupBy: [{type: 'time', interval: 'auto'}],
- tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
- });
-
- var query = builder.build();
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
- '$timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with tags OR condition', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- groupBy: [{type: 'time', interval: 'auto'}],
- tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
- });
-
- var query = builder.build();
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
- '$timeFilter GROUP BY time($interval)');
- });
- });
-
- describe('series with groupByTag', function() {
- it('should generate correct query', function() {
- var builder = new InfluxQueryBuilder({
- measurement: 'cpu',
- tags: [],
- groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
- });
-
- var query = builder.build();
- expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' +
- 'GROUP BY time($interval), "host"');
- });
- });
-
describe('when building explore queries', function() {
it('should only have measurement condition in tag keys query given query with measurement', function() {
@@ -126,8 +16,7 @@ describe('InfluxQueryBuilder', function() {
it('should handle regex measurement in tag keys query', function() {
var builder = new InfluxQueryBuilder({
- measurement: '/.*/',
- tags: []
+ measurement: '/.*/', tags: []
});
var query = builder.buildExploreQuery('TAG_KEYS');
expect(query).to.be('SHOW TAG KEYS FROM /.*/');
@@ -170,7 +59,10 @@ describe('InfluxQueryBuilder', function() {
});
it('should switch to regex operator in tag condition', function() {
- var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]});
+ var builder = new InfluxQueryBuilder({
+ measurement: 'cpu',
+ tags: [{key: 'host', value: '/server.*/'}]
+ });
var query = builder.buildExploreQuery('TAG_VALUES', 'app');
expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
});
diff --git a/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts
new file mode 100644
index 00000000000..ee939fdab42
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/query_part_specs.ts
@@ -0,0 +1,41 @@
+
+import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+
+import queryPart = require('../query_part');
+
+describe('InfluxQueryPart', () => {
+
+ describe('series with mesurement only', () => {
+ it('should handle nested function parts', () => {
+ var part = queryPart.create({
+ type: 'derivative',
+ params: ['10s'],
+ });
+
+ expect(part.text).to.be('derivative(10s)');
+ expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)');
+ });
+
+ it('should handle suffirx parts', () => {
+ var part = queryPart.create({
+ type: 'math',
+ params: ['/ 100'],
+ });
+
+ expect(part.text).to.be('math(/ 100)');
+ expect(part.render('mean(value)')).to.be('mean(value) / 100');
+ });
+
+ it('should handle alias parts', () => {
+ var part = queryPart.create({
+ type: 'alias',
+ params: ['test'],
+ });
+
+ expect(part.text).to.be('alias(test)');
+ expect(part.render('mean(value)')).to.be('mean(value) AS "test"');
+ });
+
+ });
+
+});
diff --git a/public/app/plugins/datasource/prometheus/datasource.js b/public/app/plugins/datasource/prometheus/datasource.js
index 2c9279cee96..509b0ce9970 100644
--- a/public/app/plugins/datasource/prometheus/datasource.js
+++ b/public/app/plugins/datasource/prometheus/datasource.js
@@ -111,11 +111,9 @@ function (angular, _, moment, dateMath) {
var url = '/api/v1/label/__name__/values';
return this._request('GET', url).then(function(result) {
- var suggestData = _.filter(result.data.data, function(metricName) {
- return metricName.indexOf(query) !== 1;
+ return _.filter(result.data.data, function (metricName) {
+ return metricName.indexOf(query) !== 1;
});
-
- return suggestData;
});
};
diff --git a/public/app/plugins/panels/graph/axisEditor.html b/public/app/plugins/panels/graph/axisEditor.html
index 41ea8dd3696..d8a537dda46 100644
--- a/public/app/plugins/panels/graph/axisEditor.html
+++ b/public/app/plugins/panels/graph/axisEditor.html
@@ -6,7 +6,7 @@
Left Y
-
+
Unit
-
- Grid Max
-
-
-
-
-
- Min
-
-
-
-
Scale type
@@ -46,12 +30,36 @@
@@ -150,9 +167,9 @@
-
-
diff --git a/public/app/plugins/panels/graph/graph.tooltip.js b/public/app/plugins/panels/graph/graph.tooltip.js
index 8a747b61e0c..c56d7ce8c30 100644
--- a/public/app/plugins/panels/graph/graph.tooltip.js
+++ b/public/app/plugins/panels/graph/graph.tooltip.js
@@ -52,6 +52,11 @@ function ($) {
continue;
}
+ if (!series.data.length || (scope.panel.legend.hideZero && series.allIsZero)) {
+ results.push({ hidden: true });
+ continue;
+ }
+
hoverIndex = this.findHoverIndexFromData(pos.x, series);
results.time = series.data[hoverIndex][0];
diff --git a/public/app/plugins/panels/graph/legend.js b/public/app/plugins/panels/graph/legend.js
index 000fc623303..b41717ee792 100644
--- a/public/app/plugins/panels/graph/legend.js
+++ b/public/app/plugins/panels/graph/legend.js
@@ -137,6 +137,10 @@ function (angular, _, $) {
if (!series.legend) {
continue;
}
+ // ignore zero series
+ if (panel.legend.hideZero && series.allIsZero) {
+ continue;
+ }
var html = '
-
diff --git a/public/app/plugins/panels/table/editor.ts b/public/app/plugins/panels/table/editor.ts
index db41ead4710..1523eb2ef12 100644
--- a/public/app/plugins/panels/table/editor.ts
+++ b/public/app/plugins/panels/table/editor.ts
@@ -8,93 +8,101 @@ import moment = require('moment');
import {transformers} from './transformers';
-export function tablePanelEditor() {
+export class TablePanelEditorCtrl {
+
+ /** @ngInject */
+ constructor($scope, $q, uiSegmentSrv) {
+ $scope.transformers = transformers;
+ $scope.unitFormats = kbn.getUnitFormats();
+ $scope.colorModes = [
+ {text: 'Disabled', value: null},
+ {text: 'Cell', value: 'cell'},
+ {text: 'Value', value: 'value'},
+ {text: 'Row', value: 'row'},
+ ];
+ $scope.columnTypes = [
+ {text: 'Number', value: 'number'},
+ {text: 'String', value: 'string'},
+ {text: 'Date', value: 'date'},
+ ];
+ $scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
+ $scope.dateFormats = [
+ {text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
+ {text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
+ {text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
+ ];
+
+ $scope.addColumnSegment = uiSegmentSrv.newPlusButton();
+
+ $scope.getColumnOptions = function() {
+ if (!$scope.dataRaw) {
+ return $q.when([]);
+ }
+ var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw);
+ var segments = _.map(columns, (c: any) => uiSegmentSrv.newSegment({value: c.text}));
+ return $q.when(segments);
+ };
+
+ $scope.addColumn = function() {
+ $scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value});
+ $scope.render();
+
+ var plusButton = uiSegmentSrv.newPlusButton();
+ $scope.addColumnSegment.html = plusButton.html;
+ };
+
+ $scope.transformChanged = function() {
+ $scope.panel.columns = [];
+ $scope.render();
+ };
+
+ $scope.removeColumn = function(column) {
+ $scope.panel.columns = _.without($scope.panel.columns, column);
+ $scope.render();
+ };
+
+ $scope.setUnitFormat = function(column, subItem) {
+ column.unit = subItem.value;
+ $scope.render();
+ };
+
+ $scope.addColumnStyle = function() {
+ var columnStyleDefaults = {
+ unit: 'short',
+ type: 'number',
+ decimals: 2,
+ colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+ colorMode: null,
+ pattern: '/.*/',
+ dateFormat: 'YYYY-MM-DD HH:mm:ss',
+ thresholds: [],
+ };
+
+ $scope.panel.styles.push(angular.copy(columnStyleDefaults));
+ };
+
+ $scope.removeColumnStyle = function(style) {
+ $scope.panel.styles = _.without($scope.panel.styles, style);
+ };
+
+ $scope.getColumnNames = function() {
+ if (!$scope.table) {
+ return [];
+ }
+ return _.map($scope.table.columns, function(col: any) {
+ return col.text;
+ });
+ };
+ }
+}
+
+
+export function tablePanelEditor($q, uiSegmentSrv) {
'use strict';
return {
restrict: 'E',
scope: true,
- templateUrl: 'app/plugins/panels/table/editor.html',
- link: function(scope, elem) {
- scope.transformers = transformers;
- scope.unitFormats = kbn.getUnitFormats();
- scope.colorModes = [
- {text: 'Disabled', value: null},
- {text: 'Cell', value: 'cell'},
- {text: 'Value', value: 'value'},
- {text: 'Row', value: 'row'},
- ];
- scope.columnTypes = [
- {text: 'Number', value: 'number'},
- {text: 'String', value: 'string'},
- {text: 'Date', value: 'date'},
- ];
- scope.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
- scope.dateFormats = [
- {text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
- {text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
- {text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
- ];
-
- scope.updateColumnsMenu = function(data) {
- scope.columnsMenu = transformers[scope.panel.transform].getColumns(data);
- scope.showColumnOptions = true;
- };
-
- scope.$on('render', function(event, table, rawData) {
- scope.updateColumnsMenu(rawData);
- });
-
- scope.addColumn = function(menuItem) {
- scope.panel.columns.push({text: menuItem.text, value: menuItem.value});
- scope.render();
- };
-
- scope.transformChanged = function() {
- scope.panel.columns = [];
- scope.updateColumnsMenu();
- scope.render();
- };
-
- scope.removeColumn = function(column) {
- scope.panel.columns = _.without(scope.panel.columns, column);
- scope.render();
- };
-
- scope.setUnitFormat = function(column, subItem) {
- column.unit = subItem.value;
- scope.render();
- };
-
- scope.addColumnStyle = function() {
- var columnStyleDefaults = {
- unit: 'short',
- type: 'number',
- decimals: 2,
- colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
- colorMode: null,
- pattern: '/.*/',
- dateFormat: 'YYYY-MM-DD HH:mm:ss',
- thresholds: [],
- };
-
- scope.panel.styles.push(angular.copy(columnStyleDefaults));
- };
-
- scope.removeColumnStyle = function(style) {
- scope.panel.styles = _.without(scope.panel.styles, style);
- };
-
- scope.getColumnNames = function() {
- if (!scope.table) {
- return [];
- }
- return _.map(scope.table.columns, function(col: any) {
- return col.text;
- });
- };
-
- scope.updateColumnsMenu(scope.dataRaw);
- }
+ templateUrl: 'app/panels/table/editor.html',
+ controller: TablePanelEditorCtrl,
};
}
-
diff --git a/public/app/plugins/panels/table/module.ts b/public/app/plugins/panels/table/module.ts
index 1f215b44e55..3f6f1002018 100644
--- a/public/app/plugins/panels/table/module.ts
+++ b/public/app/plugins/panels/table/module.ts
@@ -19,6 +19,7 @@ export function tablePanel() {
link: function(scope, elem) {
var data;
var panel = scope.panel;
+ var pageCount = 0;
var formaters = [];
function getTableHeight() {
@@ -26,8 +27,11 @@ export function tablePanel() {
if (_.isString(panelHeight)) {
panelHeight = parseInt(panelHeight.replace('px', ''), 10);
}
+ if (pageCount > 1) {
+ panelHeight -= 28;
+ }
- return (panelHeight - 40) + 'px';
+ return (panelHeight - 60) + 'px';
}
function appendTableRows(tbodyElem) {
@@ -46,7 +50,7 @@ export function tablePanel() {
footerElem.empty();
var pageSize = panel.pageSize || 100;
- var pageCount = Math.ceil(data.rows.length / pageSize);
+ pageCount = Math.ceil(data.rows.length / pageSize);
if (pageCount === 1) {
return;
}
@@ -73,12 +77,10 @@ export function tablePanel() {
appendTableRows(tbodyElem);
- rootElem.css({
- 'max-height': panel.scroll ? getTableHeight() : ''
- });
-
container.css({'font-size': panel.fontSize});
appendPaginationControls(footerElem);
+
+ rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' });
}
elem.on('click', '.table-panel-page-link', switchPage);
diff --git a/public/app/plugins/panels/table/specs/transformers_specs.ts b/public/app/plugins/panels/table/specs/transformers_specs.ts
index 6e22e176e66..bb42b997d33 100644
--- a/public/app/plugins/panels/table/specs/transformers_specs.ts
+++ b/public/app/plugins/panels/table/specs/transformers_specs.ts
@@ -1,6 +1,7 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {TableModel} from '../table_model';
+import {transformers} from '../transformers';
describe('when transforming time series table', () => {
var table;
@@ -100,7 +101,11 @@ describe('when transforming time series table', () => {
describe('JSON Data', () => {
var panel = {
transform: 'json',
- columns: [{text: 'Timestamp', value: 'timestamp'}, {text: 'Message', value: 'message'}]
+ columns: [
+ {text: 'Timestamp', value: 'timestamp'},
+ {text: 'Message', value: 'message'},
+ {text: 'nested.level2', value: 'nested.level2'},
+ ]
};
var rawData = [
{
@@ -108,26 +113,42 @@ describe('when transforming time series table', () => {
datapoints: [
{
timestamp: 'time',
- message: 'message'
+ message: 'message',
+ nested: {
+ level2: 'level2-value'
+ }
}
]
}
];
- beforeEach(() => {
- table = TableModel.transform(rawData, panel);
+ describe('getColumns', function() {
+ it('should return nested properties', function() {
+ var columns = transformers['json'].getColumns(rawData);
+ expect(columns[0].text).to.be('timestamp');
+ expect(columns[1].text).to.be('message');
+ expect(columns[2].text).to.be('nested.level2');
+ });
});
- it ('should return 2 columns', () => {
- expect(table.columns.length).to.be(2);
- expect(table.columns[0].text).to.be('Timestamp');
- expect(table.columns[1].text).to.be('Message');
- });
+ describe('transform', function() {
+ beforeEach(() => {
+ table = TableModel.transform(rawData, panel);
+ });
- it ('should return 2 rows', () => {
- expect(table.rows.length).to.be(1);
- expect(table.rows[0][0]).to.be('time');
- expect(table.rows[0][1]).to.be('message');
+ it ('should return 2 columns', () => {
+ expect(table.columns.length).to.be(3);
+ expect(table.columns[0].text).to.be('Timestamp');
+ expect(table.columns[1].text).to.be('Message');
+ expect(table.columns[2].text).to.be('nested.level2');
+ });
+
+ it ('should return 2 rows', () => {
+ expect(table.rows.length).to.be(1);
+ expect(table.rows[0][0]).to.be('time');
+ expect(table.rows[0][1]).to.be('message');
+ expect(table.rows[0][2]).to.be('level2-value');
+ });
});
});
diff --git a/public/app/plugins/panels/table/transformers.ts b/public/app/plugins/panels/table/transformers.ts
index 0448b4eaef2..3845e8948aa 100644
--- a/public/app/plugins/panels/table/transformers.ts
+++ b/public/app/plugins/panels/table/transformers.ts
@@ -2,6 +2,7 @@
import moment = require('moment');
import _ = require('lodash');
+import flatten = require('app/core/utils/flatten');
import TimeSeries = require('app/core/time_series');
var transformers = {};
@@ -149,9 +150,12 @@ transformers['json'] = {
continue;
}
- for (var y = 0; y < series.datapoints.length; y++) {
+ // only look at 100 docs
+ var maxDocs = Math.min(series.datapoints.length, 100);
+ for (var y = 0; y < maxDocs; y++) {
var doc = series.datapoints[y];
- for (var propName in doc) {
+ var flattened = flatten(doc, null);
+ for (var propName in flattened) {
names[propName] = true;
}
}
@@ -177,13 +181,16 @@ transformers['json'] = {
for (y = 0; y < series.datapoints.length; y++) {
var dp = series.datapoints[y];
var values = [];
- for (z = 0; z < panel.columns.length; z++) {
- values.push(dp[panel.columns[z].value]);
- }
- if (values.length === 0) {
+ if (_.isObject(dp) && panel.columns.length > 0) {
+ var flattened = flatten(dp, null);
+ for (z = 0; z < panel.columns.length; z++) {
+ values.push(flattened[panel.columns[z].value]);
+ }
+ } else {
values.push(JSON.stringify(dp));
}
+
model.rows.push(values);
}
}
diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json
index 43e9e37836c..fbf2ae0dc9d 100644
--- a/public/dashboards/template_vars.json
+++ b/public/dashboards/template_vars.json
@@ -241,7 +241,7 @@
{
"type": "query",
"datasource": null,
- "refresh_on_load": false,
+ "refresh": false,
"name": "metric",
"options": [],
"includeAll": true,
diff --git a/public/test/core/utils/flatten_specs.ts b/public/test/core/utils/flatten_specs.ts
new file mode 100644
index 00000000000..01815df8607
--- /dev/null
+++ b/public/test/core/utils/flatten_specs.ts
@@ -0,0 +1,24 @@
+import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'
+
+import flatten = require('app/core/utils/flatten')
+
+describe("flatten", () => {
+
+ it('should return flatten object', () => {
+ var flattened = flatten({
+ level1: 'level1-value',
+ deeper: {
+ level2: 'level2-value',
+ deeper: {
+ level3: 'level3-value'
+ }
+ }
+ }, null);
+
+ expect(flattened['level1']).to.be('level1-value');
+ expect(flattened['deeper.level2']).to.be('level2-value');
+ expect(flattened['deeper.deeper.level3']).to.be('level3-value');
+ });
+
+});
+
diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js
index 2253f4d8ecd..23c752fe471 100644
--- a/public/test/core/utils/kbn_specs.js
+++ b/public/test/core/utils/kbn_specs.js
@@ -68,6 +68,27 @@ define([
describeValueFormat('wps', 789000000, 1000000, -1, '789M wps');
describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops');
+ describeValueFormat('s', 24, 1, 0, '24 s');
+ describeValueFormat('s', 246, 1, 0, '4.1 min');
+ describeValueFormat('s', 24567, 100, 0, '6.82 hour');
+ describeValueFormat('s', 24567890, 10000, 0, '40.62 week');
+ describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year');
+
+ describeValueFormat('m', 24, 1, 0, '24 min');
+ describeValueFormat('m', 246, 10, 0, '4.1 hour');
+ describeValueFormat('m', 6545, 10, 0, '4.55 day');
+ describeValueFormat('m', 24567, 100, 0, '2.44 week');
+ describeValueFormat('m', 24567892, 10000, 0, '46.7 year');
+
+ describeValueFormat('h', 21, 1, 0, '21 hour');
+ describeValueFormat('h', 145, 1, 0, '6.04 day');
+ describeValueFormat('h', 1234, 100, 0, '7.3 week');
+ describeValueFormat('h', 9458, 1000, 0, '1.08 year');
+
+ describeValueFormat('d', 3, 1, 0, '3 day');
+ describeValueFormat('d', 245, 100, 0, '35 week');
+ describeValueFormat('d', 2456, 10, 0, '6.73 year');
+
describe('kbn.toFixed and negative decimals', function() {
it('should treat as zero decimals', function() {
var str = kbn.toFixed(186.123, -2);
diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js
index 8da659825b8..5b2fefd384d 100644
--- a/public/test/specs/dashboardSrv-specs.js
+++ b/public/test/specs/dashboardSrv-specs.js
@@ -204,7 +204,7 @@ define([
});
it('dashboard schema version should be set to latest', function() {
- expect(model.schemaVersion).to.be(7);
+ expect(model.schemaVersion).to.be(8);
});
});
@@ -248,5 +248,90 @@ define([
expect(clone.meta).to.be(undefined);
});
});
+
+ describe('when loading dashboard with old influxdb query schema', function() {
+ var model;
+ var target;
+
+ beforeEach(function() {
+ model = _dashboardSrv.create({
+ rows: [{
+ panels: [{
+ type: 'graph',
+ targets: [{
+ "alias": "$tag_datacenter $tag_source $col",
+ "column": "value",
+ "measurement": "logins.count",
+ "fields": [
+ {
+ "func": "mean",
+ "name": "value",
+ "mathExpr": "*2",
+ "asExpr": "value"
+ },
+ {
+ "name": "one-minute",
+ "func": "mean",
+ "mathExpr": "*3",
+ "asExpr": "one-minute"
+ }
+ ],
+ "tags": [],
+ "fill": "previous",
+ "function": "mean",
+ "groupBy": [
+ {
+ "interval": "auto",
+ "type": "time"
+ },
+ {
+ "key": "source",
+ "type": "tag"
+ },
+ {
+ "type": "tag",
+ "key": "datacenter"
+ }
+ ],
+ }]
+ }]
+ }]
+ });
+
+ target = model.rows[0].panels[0].targets[0];
+ });
+
+ it('should update query schema', function() {
+ expect(target.fields).to.be(undefined);
+ expect(target.select.length).to.be(2);
+ expect(target.select[0].length).to.be(4);
+ expect(target.select[0][0].type).to.be('field');
+ expect(target.select[0][1].type).to.be('mean');
+ expect(target.select[0][2].type).to.be('math');
+ expect(target.select[0][3].type).to.be('alias');
+ });
+
+ });
+
+ describe('when creating dashboard model with missing list for annoations or templating', function() {
+ var model;
+
+ beforeEach(function() {
+ model = _dashboardSrv.create({
+ annotations: {
+ enable: true,
+ },
+ templating: {
+ enable: true
+ }
+ });
+ });
+
+ it('should add empty list', function() {
+ expect(model.annotations.list.length).to.be(0);
+ expect(model.templating.list.length).to.be(0);
+ });
+ });
+
});
});
diff --git a/public/test/specs/time_srv_specs.js b/public/test/specs/time_srv_specs.js
index 4f065af6cf8..9943aae6cc3 100644
--- a/public/test/specs/time_srv_specs.js
+++ b/public/test/specs/time_srv_specs.js
@@ -78,13 +78,20 @@ define([
});
describe('setTime', function() {
- it('should return disable refresh for absolute times', function() {
+ it('should return disable refresh if refresh is disabled for any range', function() {
_dashboard.refresh = false;
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.refresh).to.be(false);
});
+ it('should restore refresh for absolute time range', function() {
+ _dashboard.refresh = '30s';
+
+ ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' });
+ expect(_dashboard.refresh).to.be('30s');
+ });
+
it('should restore refresh after relative time range is set', function() {
_dashboard.refresh = '10s';
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])});
diff --git a/public/vendor/showdown.js b/public/vendor/showdown.js
index 9493071ca68..0286b0598f1 100644
--- a/public/vendor/showdown.js
+++ b/public/vendor/showdown.js
@@ -855,7 +855,7 @@ var _DoLists = function(text) {
// Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary:
- list = list.replace(/\n{2,}/g,"\n\n\n");;
+ list = list.replace(/\n{2,}/g,"\n\n\n");
var result = _ProcessListItems(list);
// Trim any trailing whitespace, to put the closing `$list_type>`
@@ -875,7 +875,7 @@ var _DoLists = function(text) {
var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol";
// Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary:
- var list = list.replace(/\n{2,}/g,"\n\n\n");;
+ list = list.replace(/\n{2,}/g,"\n\n\n");
var result = _ProcessListItems(list);
result = runup + "<"+list_type+">\n" + result + ""+list_type+">\n";
return result;
@@ -1451,4 +1451,4 @@ if (typeof define === 'function' && define.amd) {
define(function() {
return Showdown;
});
-}
\ No newline at end of file
+}
diff --git a/public/views/index.html b/public/views/index.html
index 2932276c62f..8b4177baa5e 100644
--- a/public/views/index.html
+++ b/public/views/index.html
@@ -9,19 +9,19 @@
Grafana
[[if .User.LightTheme]]
-
+
[[else]]
-
+
[[end]]
[[ range $css := .PluginCss ]]
[[ end ]]
-
+
-
+
diff --git a/tasks/build_task.js b/tasks/build_task.js
index 7773299a06d..54562dfbde1 100644
--- a/tasks/build_task.js
+++ b/tasks/build_task.js
@@ -13,7 +13,7 @@ module.exports = function(grunt) {
'karma:test',
'phantomjs',
'css',
- 'htmlmin:build',
+ // 'htmlmin:build',
'ngtemplates',
'cssmin:build',
'ngAnnotate:build',
@@ -34,8 +34,8 @@ module.exports = function(grunt) {
for(var key in summary){
if(summary.hasOwnProperty(key)){
- var orig = key.replace(root, root+'/[[.AppSubUrl]]');
- var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]');
+ var orig = key.replace(root, root+'/[[.AppSubUrl]]/public');
+ var revved = summary[key].replace(root, root+'/[[.AppSubUrl]]/public');
fixed[orig] = revved;
}
}
diff --git a/tasks/options/concat.js b/tasks/options/concat.js
index c15aa8a2d6e..4e6927b306a 100644
--- a/tasks/options/concat.js
+++ b/tasks/options/concat.js
@@ -27,7 +27,7 @@ module.exports = function(config) {
js: {
src: [
'<%= tempDir %>/vendor/requirejs/require.js',
- '<%= tempDir %>/app/components/require.config.js',
+ '<%= tempDir %>/app/require_config.js',
'<%= tempDir %>/app/app.js',
],
dest: '<%= genDir %>/app/app.js'