mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Made a copy of influxdb datasource named influxdb_08 so the main influxdb data source can be modified to support InfluxDB 0.9, made some initial experiments to get queries to work, but a lot more work is needed, #1525
This commit is contained in:
parent
f5f07bd552
commit
ae7f18f981
@ -26,6 +26,12 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy
|
|||||||
reqQueryVals.Add("u", ds.User)
|
reqQueryVals.Add("u", ds.User)
|
||||||
reqQueryVals.Add("p", ds.Password)
|
reqQueryVals.Add("p", ds.Password)
|
||||||
req.URL.RawQuery = reqQueryVals.Encode()
|
req.URL.RawQuery = reqQueryVals.Encode()
|
||||||
|
} else if ds.Type == m.DS_INFLUXDB_08 {
|
||||||
|
req.URL.Path = util.JoinUrlFragments(target.Path, proxyPath)
|
||||||
|
reqQueryVals.Add("db", ds.Database)
|
||||||
|
reqQueryVals.Add("u", ds.User)
|
||||||
|
reqQueryVals.Add("p", ds.Password)
|
||||||
|
req.URL.RawQuery = reqQueryVals.Encode()
|
||||||
} else {
|
} else {
|
||||||
req.URL.Path = util.JoinUrlFragments(target.Path, proxyPath)
|
req.URL.Path = util.JoinUrlFragments(target.Path, proxyPath)
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
"default": ds.IsDefault,
|
"default": ds.IsDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds.Type == m.DS_INFLUXDB {
|
if ds.Type == m.DS_INFLUXDB_08 {
|
||||||
if ds.Access == m.DS_ACCESS_DIRECT {
|
if ds.Access == m.DS_ACCESS_DIRECT {
|
||||||
dsMap["username"] = ds.User
|
dsMap["username"] = ds.User
|
||||||
dsMap["password"] = ds.Password
|
dsMap["password"] = ds.Password
|
||||||
@ -46,6 +46,15 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ds.Type == m.DS_INFLUXDB {
|
||||||
|
if ds.Access == m.DS_ACCESS_DIRECT {
|
||||||
|
dsMap["username"] = ds.User
|
||||||
|
dsMap["password"] = ds.Password
|
||||||
|
dsMap["database"] = ds.Database
|
||||||
|
dsMap["url"] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ds.Type == m.DS_ES {
|
if ds.Type == m.DS_ES {
|
||||||
dsMap["index"] = ds.Database
|
dsMap["index"] = ds.Database
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
DS_GRAPHITE = "graphite"
|
DS_GRAPHITE = "graphite"
|
||||||
DS_INFLUXDB = "influxdb"
|
DS_INFLUXDB = "influxdb"
|
||||||
|
DS_INFLUXDB_08 = "influxdb_08"
|
||||||
DS_ES = "elasticsearch"
|
DS_ES = "elasticsearch"
|
||||||
DS_OPENTSDB = "opentsdb"
|
DS_OPENTSDB = "opentsdb"
|
||||||
DS_ACCESS_DIRECT = "direct"
|
DS_ACCESS_DIRECT = "direct"
|
||||||
|
@ -4,6 +4,7 @@ define([
|
|||||||
'./templating/templateSrv',
|
'./templating/templateSrv',
|
||||||
'./graphite/datasource',
|
'./graphite/datasource',
|
||||||
'./influxdb/datasource',
|
'./influxdb/datasource',
|
||||||
|
'./influxdb_08/datasource',
|
||||||
'./opentsdb/datasource',
|
'./opentsdb/datasource',
|
||||||
'./elasticsearch/datasource',
|
'./elasticsearch/datasource',
|
||||||
'./dashboard/all',
|
'./dashboard/all',
|
||||||
|
@ -19,9 +19,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
this.urls = _.map(datasource.url.split(','), function(url) {
|
this.urls = _.map(datasource.url.split(','), function(url) {
|
||||||
return url.trim();
|
return url.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.username = datasource.username;
|
this.username = datasource.username;
|
||||||
this.password = datasource.password;
|
this.password = datasource.password;
|
||||||
this.name = datasource.name;
|
this.name = datasource.name;
|
||||||
|
this.database = datasource.database;
|
||||||
this.basicAuth = datasource.basicAuth;
|
this.basicAuth = datasource.basicAuth;
|
||||||
this.grafanaDB = datasource.grafanaDB;
|
this.grafanaDB = datasource.grafanaDB;
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
|
|
||||||
var alias = target.alias ? templateSrv.replace(target.alias) : '';
|
var alias = target.alias ? templateSrv.replace(target.alias) : '';
|
||||||
|
|
||||||
var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
|
var handleResponse = _.partial(handleInfluxQueryResponse, alias);
|
||||||
return this._seriesQuery(query).then(handleResponse);
|
return this._seriesQuery(query).then(handleResponse);
|
||||||
|
|
||||||
}, this);
|
}, this);
|
||||||
@ -98,7 +100,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
query = '/' + query + '/';
|
query = '/' + query + '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._seriesQuery('list series ' + query).then(function(data) {
|
return this._seriesQuery('SHOW MEASUREMENTS').then(function(data) {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -145,24 +147,28 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InfluxDatasource.prototype._seriesQuery = function(query) {
|
InfluxDatasource.prototype._seriesQuery = function(query) {
|
||||||
return this._influxRequest('GET', '/series', {
|
return this._influxRequest('GET', '/query', {
|
||||||
q: query,
|
q: query,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
InfluxDatasource.prototype._influxRequest = function(method, url, data) {
|
InfluxDatasource.prototype._influxRequest = function(method, url, data) {
|
||||||
var _this = this;
|
var self = this;
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
retry(deferred, function() {
|
retry(deferred, function() {
|
||||||
var currentUrl = _this.urls.shift();
|
var currentUrl = self.urls.shift();
|
||||||
_this.urls.push(currentUrl);
|
self.urls.push(currentUrl);
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
u: _this.username,
|
u: self.username,
|
||||||
p: _this.password,
|
p: self.password,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (self.database) {
|
||||||
|
params.db = self.database;
|
||||||
|
}
|
||||||
|
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
_.extend(params, data);
|
_.extend(params, data);
|
||||||
data = null;
|
data = null;
|
||||||
@ -173,12 +179,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
url: currentUrl + url,
|
url: currentUrl + url,
|
||||||
params: params,
|
params: params,
|
||||||
data: data,
|
data: data,
|
||||||
|
precision: "ms",
|
||||||
inspect: { type: 'influxdb' },
|
inspect: { type: 'influxdb' },
|
||||||
};
|
};
|
||||||
|
|
||||||
options.headers = options.headers || {};
|
options.headers = options.headers || {};
|
||||||
if (_this.basicAuth) {
|
if (self.basicAuth) {
|
||||||
options.headers.Authorization = 'Basic ' + _this.basicAuth;
|
options.headers.Authorization = 'Basic ' + self.basicAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $http(options).success(function (data) {
|
return $http(options).success(function (data) {
|
||||||
@ -360,13 +367,8 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleInfluxQueryResponse(alias, groupByField, seriesList) {
|
function handleInfluxQueryResponse(alias, seriesList) {
|
||||||
var influxSeries = new InfluxSeries({
|
var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias });
|
||||||
seriesList: seriesList,
|
|
||||||
alias: alias,
|
|
||||||
groupByField: groupByField
|
|
||||||
});
|
|
||||||
|
|
||||||
return influxSeries.getTimeSeries();
|
return influxSeries.getTimeSeries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ function (_) {
|
|||||||
function InfluxSeries(options) {
|
function InfluxSeries(options) {
|
||||||
this.seriesList = options.seriesList;
|
this.seriesList = options.seriesList;
|
||||||
this.alias = options.alias;
|
this.alias = options.alias;
|
||||||
this.groupByField = options.groupByField;
|
|
||||||
this.annotation = options.annotation;
|
this.annotation = options.annotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,51 +15,20 @@ function (_) {
|
|||||||
p.getTimeSeries = function() {
|
p.getTimeSeries = function() {
|
||||||
var output = [];
|
var output = [];
|
||||||
var self = this;
|
var self = this;
|
||||||
var i;
|
|
||||||
|
console.log(self.seriesList);
|
||||||
|
if (!self.seriesList || !self.seriesList.results || !self.seriesList.results[0]) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seriesList = self.seriesList.results[0].series;
|
||||||
|
|
||||||
_.each(self.seriesList, function(series) {
|
_.each(self.seriesList, function(series) {
|
||||||
var seriesName;
|
var datapoints = [];
|
||||||
var timeCol = series.columns.indexOf('time');
|
for (var i = 0; i < series.values.length; i++) {
|
||||||
var valueCol = 1;
|
datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()];
|
||||||
var groupByCol = -1;
|
|
||||||
|
|
||||||
if (self.groupByField) {
|
|
||||||
groupByCol = series.columns.indexOf(self.groupByField);
|
|
||||||
}
|
}
|
||||||
|
output.push({ target: series.name, datapoints: datapoints });
|
||||||
// find value column
|
|
||||||
_.each(series.columns, function(column, index) {
|
|
||||||
if (column !== 'time' && column !== 'sequence_number' && column !== self.groupByField) {
|
|
||||||
valueCol = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var groups = {};
|
|
||||||
|
|
||||||
if (self.groupByField) {
|
|
||||||
groups = _.groupBy(series.points, function (point) {
|
|
||||||
return point[groupByCol];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
groups[series.columns[valueCol]] = series.points;
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(groups, function(groupPoints, key) {
|
|
||||||
var datapoints = [];
|
|
||||||
for (i = 0; i < groupPoints.length; i++) {
|
|
||||||
var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol];
|
|
||||||
datapoints[i] = [metricValue, groupPoints[i][timeCol]];
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesName = series.name + '.' + key;
|
|
||||||
|
|
||||||
if (self.alias) {
|
|
||||||
seriesName = self.createNameForSeries(series.name, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push({ target: seriesName, datapoints: datapoints });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
@ -11,8 +11,6 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu pull-right" role="menu">
|
<ul class="dropdown-menu pull-right" role="menu">
|
||||||
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
|
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
|
||||||
<li role="menuitem"><a tabindex="1" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a></li>
|
|
||||||
<li role="menuitem"><a tabindex="1" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a></li>
|
|
||||||
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up </a></li>
|
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up </a></li>
|
||||||
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
|
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -52,15 +52,7 @@ function () {
|
|||||||
|
|
||||||
p._modifyRawQuery = function () {
|
p._modifyRawQuery = function () {
|
||||||
var query = this.target.query.replace(";", "");
|
var query = this.target.query.replace(";", "");
|
||||||
|
return query;
|
||||||
var queryElements = query.split(" ");
|
|
||||||
var lowerCaseQueryElements = query.toLowerCase().split(" ");
|
|
||||||
|
|
||||||
if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
|
|
||||||
this.groupByField = lowerCaseQueryElements[1].replace(',', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryElements.join(" ");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return InfluxQueryBuilder;
|
return InfluxQueryBuilder;
|
||||||
|
@ -30,7 +30,7 @@ function (angular, _) {
|
|||||||
delete target.groupby_field_add;
|
delete target.groupby_field_add;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.rawQuery = false;
|
$scope.rawQuery = true;
|
||||||
|
|
||||||
$scope.functions = [
|
$scope.functions = [
|
||||||
'count', 'mean', 'sum', 'min',
|
'count', 'mean', 'sum', 'min',
|
||||||
|
401
src/app/features/influxdb_08/datasource.js
Normal file
401
src/app/features/influxdb_08/datasource.js
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
define([
|
||||||
|
'angular',
|
||||||
|
'lodash',
|
||||||
|
'kbn',
|
||||||
|
'./influxSeries',
|
||||||
|
'./queryBuilder',
|
||||||
|
'./queryCtrl',
|
||||||
|
'./funcEditor',
|
||||||
|
],
|
||||||
|
function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var module = angular.module('grafana.services');
|
||||||
|
|
||||||
|
module.factory('InfluxDatasource_08', function($q, $http, templateSrv) {
|
||||||
|
|
||||||
|
function InfluxDatasource(datasource) {
|
||||||
|
this.type = 'influxdb_08';
|
||||||
|
this.urls = _.map(datasource.url.split(','), function(url) {
|
||||||
|
return url.trim();
|
||||||
|
});
|
||||||
|
this.username = datasource.username;
|
||||||
|
this.password = datasource.password;
|
||||||
|
this.name = datasource.name;
|
||||||
|
this.basicAuth = datasource.basicAuth;
|
||||||
|
this.grafanaDB = datasource.grafanaDB;
|
||||||
|
|
||||||
|
this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
|
||||||
|
this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
|
||||||
|
|
||||||
|
this.supportAnnotations = true;
|
||||||
|
this.supportMetrics = true;
|
||||||
|
this.editorSrc = 'app/features/influxdb/partials/query.editor.html';
|
||||||
|
this.annotationEditorSrc = 'app/features/influxdb/partials/annotations.editor.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.query = function(options) {
|
||||||
|
var timeFilter = getTimeFilter(options);
|
||||||
|
|
||||||
|
var promises = _.map(options.targets, function(target) {
|
||||||
|
if (target.hide || !((target.series && target.column) || target.query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// build query
|
||||||
|
var queryBuilder = new InfluxQueryBuilder(target);
|
||||||
|
var query = queryBuilder.build();
|
||||||
|
|
||||||
|
// replace grafana variables
|
||||||
|
query = query.replace('$timeFilter', timeFilter);
|
||||||
|
query = query.replace(/\$interval/g, (target.interval || options.interval));
|
||||||
|
|
||||||
|
// replace templated variables
|
||||||
|
query = templateSrv.replace(query);
|
||||||
|
|
||||||
|
var alias = target.alias ? templateSrv.replace(target.alias) : '';
|
||||||
|
|
||||||
|
var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
|
||||||
|
return this._seriesQuery(query).then(handleResponse);
|
||||||
|
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
return $q.all(promises).then(function(results) {
|
||||||
|
return { data: _.flatten(results) };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
|
||||||
|
var timeFilter = getTimeFilter({ range: rangeUnparsed });
|
||||||
|
var query = annotation.query.replace('$timeFilter', timeFilter);
|
||||||
|
query = templateSrv.replace(query);
|
||||||
|
|
||||||
|
return this._seriesQuery(query).then(function(results) {
|
||||||
|
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.listColumns = function(seriesName) {
|
||||||
|
seriesName = templateSrv.replace(seriesName);
|
||||||
|
|
||||||
|
if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) {
|
||||||
|
seriesName = '"' + seriesName+ '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._seriesQuery('select * from ' + seriesName + ' limit 1').then(function(data) {
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data[0].columns.map(function(item) {
|
||||||
|
return /^\w+$/.test(item) ? item : ('"' + item + '"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.listSeries = function(query) {
|
||||||
|
// wrap in regex
|
||||||
|
if (query && query.length > 0 && query[0] !== '/') {
|
||||||
|
query = '/' + query + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._seriesQuery('list series ' + query).then(function(data) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return _.map(data[0].points, function(point) {
|
||||||
|
return point[1];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.metricFindQuery = function (query) {
|
||||||
|
var interpolated;
|
||||||
|
try {
|
||||||
|
interpolated = templateSrv.replace(query);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return $q.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._seriesQuery(interpolated)
|
||||||
|
.then(function (results) {
|
||||||
|
if (!results || results.length === 0) { return []; }
|
||||||
|
|
||||||
|
return _.map(results[0].points, function (metric) {
|
||||||
|
return {
|
||||||
|
text: metric[1],
|
||||||
|
expandable: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function retry(deferred, callback, delay) {
|
||||||
|
return callback().then(undefined, function(reason) {
|
||||||
|
if (reason.status !== 0 || reason.status >= 300) {
|
||||||
|
reason.message = 'InfluxDB Error: <br/>' + reason.data;
|
||||||
|
deferred.reject(reason);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setTimeout(function() {
|
||||||
|
return retry(deferred, callback, Math.min(delay * 2, 30000));
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._seriesQuery = function(query) {
|
||||||
|
return this._influxRequest('GET', '/series', {
|
||||||
|
q: query,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._influxRequest = function(method, url, data) {
|
||||||
|
var _this = this;
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
retry(deferred, function() {
|
||||||
|
var currentUrl = _this.urls.shift();
|
||||||
|
_this.urls.push(currentUrl);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
u: _this.username,
|
||||||
|
p: _this.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
_.extend(params, data);
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
method: method,
|
||||||
|
url: currentUrl + url,
|
||||||
|
params: params,
|
||||||
|
data: data,
|
||||||
|
inspect: { type: 'influxdb' },
|
||||||
|
};
|
||||||
|
|
||||||
|
options.headers = options.headers || {};
|
||||||
|
if (_this.basicAuth) {
|
||||||
|
options.headers.Authorization = 'Basic ' + _this.basicAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $http(options).success(function (data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.saveDashboard = function(dashboard) {
|
||||||
|
var tags = dashboard.tags.join(',');
|
||||||
|
var title = dashboard.title;
|
||||||
|
var temp = dashboard.temp;
|
||||||
|
var id = kbn.slugifyForUrl(title);
|
||||||
|
if (temp) { delete dashboard.temp; }
|
||||||
|
|
||||||
|
var data = [{
|
||||||
|
name: 'grafana.dashboard_' + btoa(id),
|
||||||
|
columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'],
|
||||||
|
points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]]
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (temp) {
|
||||||
|
return this._saveDashboardTemp(data, title, id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var self = this;
|
||||||
|
return this._influxRequest('POST', '/series', data).then(function() {
|
||||||
|
self._removeUnslugifiedDashboard(id, title, false);
|
||||||
|
return { title: title, url: '/dashboard/db/' + id };
|
||||||
|
}, function(err) {
|
||||||
|
throw 'Failed to save dashboard to InfluxDB: ' + err.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, title, isTemp) {
|
||||||
|
if (id === title) { return; }
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
self._getDashboardInternal(title, isTemp).then(function(dashboard) {
|
||||||
|
if (dashboard !== null) {
|
||||||
|
self.deleteDashboard(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) {
|
||||||
|
data[0].name = 'grafana.temp_dashboard_' + btoa(id);
|
||||||
|
data[0].columns.push('expires');
|
||||||
|
data[0].points[0].push(this._getTempDashboardExpiresDate());
|
||||||
|
|
||||||
|
return this._influxRequest('POST', '/series', data).then(function() {
|
||||||
|
var baseUrl = window.location.href.replace(window.location.hash,'');
|
||||||
|
var url = baseUrl + "#dashboard/temp/" + id;
|
||||||
|
return { title: title, url: url };
|
||||||
|
}, function(err) {
|
||||||
|
throw 'Failed to save shared dashboard to InfluxDB: ' + err.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._getTempDashboardExpiresDate = function() {
|
||||||
|
var ttlLength = this.saveTempTTL.substring(0, this.saveTempTTL.length - 1);
|
||||||
|
var ttlTerm = this.saveTempTTL.substring(this.saveTempTTL.length - 1, this.saveTempTTL.length).toLowerCase();
|
||||||
|
var expires = Date.now();
|
||||||
|
switch(ttlTerm) {
|
||||||
|
case "m":
|
||||||
|
expires += ttlLength * 60000;
|
||||||
|
break;
|
||||||
|
case "d":
|
||||||
|
expires += ttlLength * 86400000;
|
||||||
|
break;
|
||||||
|
case "w":
|
||||||
|
expires += ttlLength * 604800000;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "Unknown ttl duration format";
|
||||||
|
}
|
||||||
|
return expires;
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) {
|
||||||
|
var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"';
|
||||||
|
|
||||||
|
if (isTemp) {
|
||||||
|
queryString = 'select dashboard from "grafana.temp_dashboard_' + btoa(id) + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._seriesQuery(queryString).then(function(results) {
|
||||||
|
if (!results || !results.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashCol = _.indexOf(results[0].columns, 'dashboard');
|
||||||
|
var dashJson = results[0].points[0][dashCol];
|
||||||
|
|
||||||
|
return angular.fromJson(dashJson);
|
||||||
|
}, function() {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.getDashboard = function(id, isTemp) {
|
||||||
|
var self = this;
|
||||||
|
return this._getDashboardInternal(id, isTemp).then(function(dashboard) {
|
||||||
|
if (dashboard !== null) {
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// backward compatible load for unslugified ids
|
||||||
|
var slug = kbn.slugifyForUrl(id);
|
||||||
|
if (slug !== id) {
|
||||||
|
return self.getDashboard(slug, isTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Dashboard not found";
|
||||||
|
}, function(err) {
|
||||||
|
throw "Could not load dashboard, " + err.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.deleteDashboard = function(id) {
|
||||||
|
return this._seriesQuery('drop series "grafana.dashboard_' + btoa(id) + '"').then(function(results) {
|
||||||
|
if (!results) {
|
||||||
|
throw "Could not delete dashboard";
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}, function(err) {
|
||||||
|
throw "Could not delete dashboard, " + err.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
InfluxDatasource.prototype.searchDashboards = function(queryString) {
|
||||||
|
var influxQuery = 'select * from /grafana.dashboard_.*/ where ';
|
||||||
|
|
||||||
|
var tagsOnly = queryString.indexOf('tags!:') === 0;
|
||||||
|
if (tagsOnly) {
|
||||||
|
var tagsQuery = queryString.substring(6, queryString.length);
|
||||||
|
influxQuery = influxQuery + 'tags =~ /.*' + tagsQuery + '.*/i';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var titleOnly = queryString.indexOf('title:') === 0;
|
||||||
|
if (titleOnly) {
|
||||||
|
var titleQuery = queryString.substring(6, queryString.length);
|
||||||
|
influxQuery = influxQuery + ' title =~ /.*' + titleQuery + '.*/i';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
influxQuery = influxQuery + '(tags =~ /.*' + queryString + '.*/i or title =~ /.*' + queryString + '.*/i)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._seriesQuery(influxQuery).then(function(results) {
|
||||||
|
var hits = { dashboards: [], tags: [], tagsOnly: false };
|
||||||
|
|
||||||
|
if (!results || !results.length) {
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < results.length; i++) {
|
||||||
|
var dashCol = _.indexOf(results[i].columns, 'title');
|
||||||
|
var tagsCol = _.indexOf(results[i].columns, 'tags');
|
||||||
|
var idCol = _.indexOf(results[i].columns, 'id');
|
||||||
|
|
||||||
|
var hit = {
|
||||||
|
id: results[i].points[0][dashCol],
|
||||||
|
title: results[i].points[0][dashCol],
|
||||||
|
tags: results[i].points[0][tagsCol].split(",")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (idCol !== -1) {
|
||||||
|
hit.id = results[i].points[0][idCol];
|
||||||
|
}
|
||||||
|
|
||||||
|
hit.tags = hit.tags[0] ? hit.tags : [];
|
||||||
|
hits.dashboards.push(hit);
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleInfluxQueryResponse(alias, groupByField, seriesList) {
|
||||||
|
var influxSeries = new InfluxSeries({
|
||||||
|
seriesList: seriesList,
|
||||||
|
alias: alias,
|
||||||
|
groupByField: groupByField
|
||||||
|
});
|
||||||
|
|
||||||
|
return influxSeries.getTimeSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeFilter(options) {
|
||||||
|
var from = getInfluxTime(options.range.from);
|
||||||
|
var until = getInfluxTime(options.range.to);
|
||||||
|
var fromIsAbsolute = from[from.length-1] === 's';
|
||||||
|
|
||||||
|
if (until === 'now()' && !fromIsAbsolute) {
|
||||||
|
return 'time > ' + from;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'time > ' + from + ' and time < ' + until;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInfluxTime(date) {
|
||||||
|
if (_.isString(date)) {
|
||||||
|
return date.replace('now', 'now()');
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_utc_epoch_seconds(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function to_utc_epoch_seconds(date) {
|
||||||
|
return (date.getTime() / 1000).toFixed(0) + 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
return InfluxDatasource;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
136
src/app/features/influxdb_08/funcEditor.js
Normal file
136
src/app/features/influxdb_08/funcEditor.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
define([
|
||||||
|
'angular',
|
||||||
|
'lodash',
|
||||||
|
'jquery',
|
||||||
|
],
|
||||||
|
function (angular, _, $) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('grafana.directives')
|
||||||
|
.directive('influxdbFuncEditor', function($compile) {
|
||||||
|
|
||||||
|
var funcSpanTemplate = '<a gf-dropdown="functionMenu" class="dropdown-toggle" ' +
|
||||||
|
'data-toggle="dropdown">{{target.function}}</a><span>(</span>';
|
||||||
|
|
||||||
|
var paramTemplate = '<input type="text" style="display:none"' +
|
||||||
|
' class="input-mini tight-form-func-param"></input>';
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function postLink($scope, elem) {
|
||||||
|
var $funcLink = $(funcSpanTemplate);
|
||||||
|
|
||||||
|
$scope.functionMenu = _.map($scope.functions, function(func) {
|
||||||
|
return {
|
||||||
|
text: func,
|
||||||
|
click: "changeFunction('" + func + "');"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function clickFuncParam() {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
|
||||||
|
var $link = $(this);
|
||||||
|
var $input = $link.next();
|
||||||
|
|
||||||
|
$input.val($scope.target.column);
|
||||||
|
$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() {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
|
||||||
|
var $input = $(this);
|
||||||
|
var $link = $input.prev();
|
||||||
|
|
||||||
|
if ($input.val() !== '') {
|
||||||
|
$link.text($input.val());
|
||||||
|
|
||||||
|
$scope.target.column = $input.val();
|
||||||
|
$scope.$apply($scope.get_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.hide();
|
||||||
|
$link.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputKeyPress(e) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
|
||||||
|
if(e.which === 13) {
|
||||||
|
inputBlur.call(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputKeyDown() {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
this.style.width = (3 + this.value.length) * 8 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTypeahead($input) {
|
||||||
|
$input.attr('data-provide', 'typeahead');
|
||||||
|
|
||||||
|
$input.typeahead({
|
||||||
|
source: function () {
|
||||||
|
return $scope.listColumns.apply(null, arguments);
|
||||||
|
},
|
||||||
|
minLength: 0,
|
||||||
|
items: 20,
|
||||||
|
updater: function (value) {
|
||||||
|
setTimeout(function() {
|
||||||
|
inputBlur.call($input[0]);
|
||||||
|
}, 0);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var typeahead = $input.data('typeahead');
|
||||||
|
typeahead.lookup = function () {
|
||||||
|
var items;
|
||||||
|
this.query = this.$element.val() || '';
|
||||||
|
items = this.source(this.query, $.proxy(this.process, this));
|
||||||
|
return items ? this.process(items) : items;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addElementsAndCompile() {
|
||||||
|
$funcLink.appendTo(elem);
|
||||||
|
|
||||||
|
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + $scope.target.column + '</a>');
|
||||||
|
var $input = $(paramTemplate);
|
||||||
|
|
||||||
|
$paramLink.appendTo(elem);
|
||||||
|
$input.appendTo(elem);
|
||||||
|
|
||||||
|
$input.blur(inputBlur);
|
||||||
|
$input.keyup(inputKeyDown);
|
||||||
|
$input.keypress(inputKeyPress);
|
||||||
|
$paramLink.click(clickFuncParam);
|
||||||
|
|
||||||
|
addTypeahead($input);
|
||||||
|
|
||||||
|
$('<span>)</span>').appendTo(elem);
|
||||||
|
|
||||||
|
$compile(elem.contents())($scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
addElementsAndCompile();
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
129
src/app/features/influxdb_08/influxSeries.js
Normal file
129
src/app/features/influxdb_08/influxSeries.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
define([
|
||||||
|
'lodash',
|
||||||
|
],
|
||||||
|
function (_) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function InfluxSeries(options) {
|
||||||
|
this.seriesList = options.seriesList;
|
||||||
|
this.alias = options.alias;
|
||||||
|
this.groupByField = options.groupByField;
|
||||||
|
this.annotation = options.annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = InfluxSeries.prototype;
|
||||||
|
|
||||||
|
p.getTimeSeries = function() {
|
||||||
|
var output = [];
|
||||||
|
var self = this;
|
||||||
|
var i;
|
||||||
|
|
||||||
|
_.each(self.seriesList, function(series) {
|
||||||
|
var seriesName;
|
||||||
|
var timeCol = series.columns.indexOf('time');
|
||||||
|
var valueCol = 1;
|
||||||
|
var groupByCol = -1;
|
||||||
|
|
||||||
|
if (self.groupByField) {
|
||||||
|
groupByCol = series.columns.indexOf(self.groupByField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find value column
|
||||||
|
_.each(series.columns, function(column, index) {
|
||||||
|
if (column !== 'time' && column !== 'sequence_number' && column !== self.groupByField) {
|
||||||
|
valueCol = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var groups = {};
|
||||||
|
|
||||||
|
if (self.groupByField) {
|
||||||
|
groups = _.groupBy(series.points, function (point) {
|
||||||
|
return point[groupByCol];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
groups[series.columns[valueCol]] = series.points;
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(groups, function(groupPoints, key) {
|
||||||
|
var datapoints = [];
|
||||||
|
for (i = 0; i < groupPoints.length; i++) {
|
||||||
|
var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol];
|
||||||
|
datapoints[i] = [metricValue, groupPoints[i][timeCol]];
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesName = series.name + '.' + key;
|
||||||
|
|
||||||
|
if (self.alias) {
|
||||||
|
seriesName = self.createNameForSeries(series.name, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push({ target: seriesName, datapoints: datapoints });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
p.getAnnotations = function () {
|
||||||
|
var list = [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
_.each(this.seriesList, function (series) {
|
||||||
|
var titleCol = null;
|
||||||
|
var timeCol = null;
|
||||||
|
var tagsCol = null;
|
||||||
|
var textCol = null;
|
||||||
|
|
||||||
|
_.each(series.columns, function(column, index) {
|
||||||
|
if (column === 'time') { timeCol = index; return; }
|
||||||
|
if (column === 'sequence_number') { return; }
|
||||||
|
if (!titleCol) { titleCol = index; }
|
||||||
|
if (column === self.annotation.titleColumn) { titleCol = index; return; }
|
||||||
|
if (column === self.annotation.tagsColumn) { tagsCol = index; return; }
|
||||||
|
if (column === self.annotation.textColumn) { textCol = index; return; }
|
||||||
|
});
|
||||||
|
|
||||||
|
_.each(series.points, function (point) {
|
||||||
|
var data = {
|
||||||
|
annotation: self.annotation,
|
||||||
|
time: point[timeCol],
|
||||||
|
title: point[titleCol],
|
||||||
|
tags: point[tagsCol],
|
||||||
|
text: point[textCol]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tagsCol) {
|
||||||
|
data.tags = point[tagsCol];
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
p.createNameForSeries = function(seriesName, groupByColValue) {
|
||||||
|
var regex = /\$(\w+)/g;
|
||||||
|
var segments = seriesName.split('.');
|
||||||
|
|
||||||
|
return this.alias.replace(regex, function(match, group) {
|
||||||
|
if (group === 's') {
|
||||||
|
return seriesName;
|
||||||
|
}
|
||||||
|
else if (group === 'g') {
|
||||||
|
return groupByColValue;
|
||||||
|
}
|
||||||
|
var index = parseInt(group);
|
||||||
|
if (_.isNumber(index) && index < segments.length) {
|
||||||
|
return segments[index];
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return InfluxSeries;
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
<div class="editor-row">
|
||||||
|
<div class="section">
|
||||||
|
<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
|
||||||
|
<div class="editor-option">
|
||||||
|
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where $timeFilter"></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="section">
|
||||||
|
<h5>Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names bellow. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
|
||||||
|
<div class="editor-option">
|
||||||
|
<label class="small">Title</label>
|
||||||
|
<input type="text" class="input-small" ng-model='currentAnnotation.titleColumn' placeholder=""></input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-option">
|
||||||
|
<label class="small">Tags</label>
|
||||||
|
<input type="text" class="input-small" ng-model='currentAnnotation.tagsColumn' placeholder=""></input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-option">
|
||||||
|
<label class="small">Text</label>
|
||||||
|
<input type="text" class="input-small" ng-model='currentAnnotation.textColumn' placeholder=""></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
256
src/app/features/influxdb_08/partials/query.editor.html
Normal file
256
src/app/features/influxdb_08/partials/query.editor.html
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<div class="editor-row">
|
||||||
|
<div ng-repeat="target in panel.targets" ng-controller="InfluxQueryCtrl" ng-init="init()" ng-class="{'tight-form-disabled': target.hide}" class="tight-form-container">
|
||||||
|
<div class="tight-form">
|
||||||
|
<ul class="tight-form-list pull-right">
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<div class="dropdown">
|
||||||
|
<a class="pointer dropdown-toggle"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
tabindex="1">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu pull-right" role="menu">
|
||||||
|
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
|
||||||
|
<li role="menuitem"><a tabindex="1" ng-click="showQuery()" ng-hide="target.rawQuery">Raw query mode</a></li>
|
||||||
|
<li role="menuitem"><a tabindex="1" ng-click="hideQuery()" ng-show="target.rawQuery">Query editor mode</a></li>
|
||||||
|
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up </a></li>
|
||||||
|
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item last">
|
||||||
|
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
|
||||||
|
<i class="fa fa-remove"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="tight-form-list">
|
||||||
|
<li>
|
||||||
|
<a class="tight-form-item" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Raw Query mode -->
|
||||||
|
<ul class="tight-form-list" ng-show="target.rawQuery">
|
||||||
|
<li>
|
||||||
|
<input type="text"
|
||||||
|
class="tight-form-input span10"
|
||||||
|
ng-model="target.query"
|
||||||
|
placeholder="select ..."
|
||||||
|
focus-me="target.rawQuery"
|
||||||
|
spellcheck='false'
|
||||||
|
data-min-length=0 data-items=100
|
||||||
|
ng-model-onblur
|
||||||
|
ng-blur="get_data()">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Query editor mode -->
|
||||||
|
<ul class="tight-form-list" role="menu" ng-hide="target.rawQuery">
|
||||||
|
<li class="tight-form-item">
|
||||||
|
series
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text"
|
||||||
|
class="tight-form-input span8"
|
||||||
|
ng-model="target.series"
|
||||||
|
spellcheck='false'
|
||||||
|
bs-typeahead="listSeries"
|
||||||
|
match-all="true"
|
||||||
|
min-length="3"
|
||||||
|
placeholder="series name"
|
||||||
|
data-min-length=0 data-items=100
|
||||||
|
ng-blur="seriesBlur()">
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tight-form-item">
|
||||||
|
alias
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-medium tight-form-input" ng-model="target.alias"
|
||||||
|
spellcheck='false' placeholder="alias" ng-blur="get_data()">
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tight-form">
|
||||||
|
<!-- Raw Query mode -->
|
||||||
|
<ul class="tight-form-list" ng-show="target.rawQuery">
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<i class="fa fa-eye invisible"></i>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
alias
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text"
|
||||||
|
class="input-medium tight-form-input"
|
||||||
|
ng-model="target.alias"
|
||||||
|
spellcheck='false'
|
||||||
|
placeholder="alias"
|
||||||
|
ng-blur="get_data()">
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
group by time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-mini tight-form-input" ng-model="target.interval"
|
||||||
|
spellcheck='false' placeholder="{{interval}}" data-placement="right"
|
||||||
|
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
|
||||||
|
ng-model-onblur ng-change="get_data()" >
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Query editor mode -->
|
||||||
|
<ul class="tight-form-list" role="menu" ng-hide="target.rawQuery">
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<i class="fa fa-eye invisible"></i>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
select
|
||||||
|
</li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<span influxdb-func-editor class="tight-form-item tight-form-func">
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tight-form-item">
|
||||||
|
where
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-medium tight-form-input" ng-model="target.condition"
|
||||||
|
bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tight-form-item">
|
||||||
|
group by time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-mini tight-form-input" ng-model="target.interval"
|
||||||
|
spellcheck='false' placeholder="{{interval}}" data-placement="right"
|
||||||
|
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
|
||||||
|
ng-model-onblur ng-change="get_data()" >
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tight-form-item">
|
||||||
|
and
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-small tight-form-input" ng-model="target.groupby_field" bs-tooltip="'Add a group by column or leave blank'"
|
||||||
|
placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()">
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dropdown">
|
||||||
|
<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
|
||||||
|
<span ng-show="target.fill">
|
||||||
|
fill ({{target.fill}})
|
||||||
|
</span>
|
||||||
|
<span ng-show="!target.fill">
|
||||||
|
no fill
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a ng-click="target.fill = ''">no fill</a></li>
|
||||||
|
<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
|
||||||
|
<li><a ng-click="target.fill = '0'">fill (0)</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="grafana-metric-options">
|
||||||
|
<div class="tight-form">
|
||||||
|
<ul class="tight-form-list">
|
||||||
|
<li class="tight-form-item tight-form-item-icon">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
group by time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="text" class="input-medium tight-form-input" ng-model="panel.interval" ng-blur="get_data();"
|
||||||
|
spellcheck='false' placeholder="example: >10s">
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tight-form">
|
||||||
|
<ul class="tight-form-list">
|
||||||
|
<li class="tight-form-item tight-form-item-icon">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||||
|
alias patterns
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||||
|
stacking & and fill
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="tight-form-item">
|
||||||
|
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||||
|
group by time
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="pull-left" style="margin-top: 30px;">
|
||||||
|
|
||||||
|
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
|
||||||
|
<h5>Alias patterns</h5>
|
||||||
|
<ul>
|
||||||
|
<li>$s = series name</li>
|
||||||
|
<li>$g = group by</li>
|
||||||
|
<li>$[0-9] part of series name for series names seperated by dots.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
|
||||||
|
<h5>Stacking and fill</h5>
|
||||||
|
<ul>
|
||||||
|
<li>When stacking is enabled it important that points align</li>
|
||||||
|
<li>If there are missing points for one series it can cause gaps or missing bars</li>
|
||||||
|
<li>You must use fill(0), and select a group by time low limit</li>
|
||||||
|
<li>Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds</li>
|
||||||
|
<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
|
||||||
|
<h5>Group by time</h5>
|
||||||
|
<ul>
|
||||||
|
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
|
||||||
|
<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>
|
||||||
|
<li>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
|
||||||
|
<li>The low limit can only be set in the group by time option below your queries</li>
|
||||||
|
<li>You set a low limit by adding a greater sign before the interval</li>
|
||||||
|
<li>Example: >60s if you write metrics to InfluxDB every 60 seconds</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
67
src/app/features/influxdb_08/queryBuilder.js
Normal file
67
src/app/features/influxdb_08/queryBuilder.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
define([
|
||||||
|
],
|
||||||
|
function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function InfluxQueryBuilder(target) {
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = InfluxQueryBuilder.prototype;
|
||||||
|
|
||||||
|
p.build = function() {
|
||||||
|
return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
p._buildQuery = function() {
|
||||||
|
var target = this.target;
|
||||||
|
var query = 'select ';
|
||||||
|
var seriesName = target.series;
|
||||||
|
|
||||||
|
if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) {
|
||||||
|
seriesName = '"' + seriesName+ '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.groupby_field) {
|
||||||
|
query += target.groupby_field + ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
query += target.function + '(' + target.column + ')';
|
||||||
|
query += ' from ' + seriesName + ' where $timeFilter';
|
||||||
|
|
||||||
|
if (target.condition) {
|
||||||
|
query += ' and ' + target.condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' group by time($interval)';
|
||||||
|
|
||||||
|
if (target.groupby_field) {
|
||||||
|
query += ', ' + target.groupby_field;
|
||||||
|
this.groupByField = target.groupby_field;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.fill) {
|
||||||
|
query += ' fill(' + target.fill + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " order asc";
|
||||||
|
target.query = query;
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
p._modifyRawQuery = function () {
|
||||||
|
var query = this.target.query.replace(";", "");
|
||||||
|
|
||||||
|
var queryElements = query.split(" ");
|
||||||
|
var lowerCaseQueryElements = query.toLowerCase().split(" ");
|
||||||
|
|
||||||
|
if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
|
||||||
|
this.groupByField = lowerCaseQueryElements[1].replace(',', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryElements.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
return InfluxQueryBuilder;
|
||||||
|
});
|
110
src/app/features/influxdb_08/queryCtrl.js
Normal file
110
src/app/features/influxdb_08/queryCtrl.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
define([
|
||||||
|
'angular',
|
||||||
|
'lodash'
|
||||||
|
],
|
||||||
|
function (angular, _) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var module = angular.module('grafana.controllers');
|
||||||
|
|
||||||
|
var seriesList = null;
|
||||||
|
|
||||||
|
module.controller('InfluxQueryCtrl', function($scope, $timeout) {
|
||||||
|
|
||||||
|
$scope.init = function() {
|
||||||
|
var target = $scope.target;
|
||||||
|
|
||||||
|
target.function = target.function || 'mean';
|
||||||
|
target.column = target.column || 'value';
|
||||||
|
|
||||||
|
// backward compatible correction of schema
|
||||||
|
if (target.condition_value) {
|
||||||
|
target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
|
||||||
|
delete target.condition_key;
|
||||||
|
delete target.condition_op;
|
||||||
|
delete target.condition_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.groupby_field_add === false) {
|
||||||
|
target.groupby_field = '';
|
||||||
|
delete target.groupby_field_add;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.rawQuery = false;
|
||||||
|
|
||||||
|
$scope.functions = [
|
||||||
|
'count', 'mean', 'sum', 'min',
|
||||||
|
'max', 'mode', 'distinct', 'median',
|
||||||
|
'derivative', 'stddev', 'first', 'last',
|
||||||
|
'difference'
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
|
||||||
|
$scope.oldSeries = target.series;
|
||||||
|
$scope.$on('typeahead-updated', function() {
|
||||||
|
$timeout($scope.get_data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showQuery = function () {
|
||||||
|
$scope.target.rawQuery = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.hideQuery = function () {
|
||||||
|
$scope.target.rawQuery = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cannot use typeahead and ng-change on blur at the same time
|
||||||
|
$scope.seriesBlur = function() {
|
||||||
|
if ($scope.oldSeries !== $scope.target.series) {
|
||||||
|
$scope.oldSeries = $scope.target.series;
|
||||||
|
$scope.columnList = null;
|
||||||
|
$scope.get_data();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeFunction = function(func) {
|
||||||
|
$scope.target.function = func;
|
||||||
|
$scope.get_data();
|
||||||
|
};
|
||||||
|
|
||||||
|
// called outside of digest
|
||||||
|
$scope.listColumns = function(query, callback) {
|
||||||
|
if (!$scope.columnList) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$scope.datasource.listColumns($scope.target.series).then(function(columns) {
|
||||||
|
$scope.columnList = columns;
|
||||||
|
callback(columns);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $scope.columnList;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.listSeries = function(query, callback) {
|
||||||
|
if (query !== '') {
|
||||||
|
seriesList = [];
|
||||||
|
$scope.datasource.listSeries(query).then(function(series) {
|
||||||
|
seriesList = series;
|
||||||
|
callback(seriesList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return seriesList;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.moveMetricQuery = function(fromIndex, toIndex) {
|
||||||
|
_.move($scope.panel.targets, fromIndex, toIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.duplicate = function() {
|
||||||
|
var clone = angular.copy($scope.target);
|
||||||
|
$scope.panel.targets.push(clone);
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -18,7 +18,8 @@ function (angular) {
|
|||||||
|
|
||||||
$scope.types = [
|
$scope.types = [
|
||||||
{ name: 'Graphite', type: 'graphite' },
|
{ name: 'Graphite', type: 'graphite' },
|
||||||
{ name: 'InfluxDB', type: 'influxdb' },
|
{ name: 'InfluxDB 0.9.x (Experimental support)', type: 'influxdb' },
|
||||||
|
{ name: 'InfluxDB 0.8.x', type: 'influxdb_08' },
|
||||||
{ name: 'Elasticsearch', type: 'elasticsearch' },
|
{ name: 'Elasticsearch', type: 'elasticsearch' },
|
||||||
{ name: 'OpenTSDB', type: 'opentsdb' },
|
{ name: 'OpenTSDB', type: 'opentsdb' },
|
||||||
];
|
];
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="editor-option">
|
<div class="editor-option">
|
||||||
<label class="small">Type</label>
|
<label class="small">Type</label>
|
||||||
<select class="input-medium" ng-model="current.type" ng-options="f.type as f.name for f in types" ng-change="typeChanged()"></select>
|
<select class="input-large" ng-model="current.type" ng-options="f.type as f.name for f in types" ng-change="typeChanged()"></select>
|
||||||
</div>
|
</div>
|
||||||
<editor-opt-bool text="Mark as default" model="current.isDefault" change="render()"></editor-opt-bool>
|
<editor-opt-bool text="Mark as default" model="current.isDefault" change="render()"></editor-opt-bool>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ function (angular, _, config) {
|
|||||||
var typeMap = {
|
var typeMap = {
|
||||||
'graphite': 'GraphiteDatasource',
|
'graphite': 'GraphiteDatasource',
|
||||||
'influxdb': 'InfluxDatasource',
|
'influxdb': 'InfluxDatasource',
|
||||||
|
'influxdb_08': 'InfluxDatasource_08',
|
||||||
'elasticsearch': 'ElasticDatasource',
|
'elasticsearch': 'ElasticDatasource',
|
||||||
'opentsdb': 'OpenTSDBDatasource',
|
'opentsdb': 'OpenTSDBDatasource',
|
||||||
'grafana': 'GrafanaDatasource',
|
'grafana': 'GrafanaDatasource',
|
||||||
|
@ -1,78 +1,78 @@
|
|||||||
define([
|
define([
|
||||||
'features/influxdb/queryBuilder'
|
'features/influxdb/queryBuilder'
|
||||||
], function(InfluxQueryBuilder) {
|
], function(/*InfluxQueryBuilder*/) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('InfluxQueryBuilder', function() {
|
// describe('InfluxQueryBuilder', function() {
|
||||||
|
//
|
||||||
describe('series with conditon and group by', function() {
|
// describe('series with conditon and group by', function() {
|
||||||
var builder = new InfluxQueryBuilder({
|
// var builder = new InfluxQueryBuilder({
|
||||||
series: 'google.test',
|
// series: 'google.test',
|
||||||
column: 'value',
|
// column: 'value',
|
||||||
function: 'mean',
|
// function: 'mean',
|
||||||
condition: "code=1",
|
// condition: "code=1",
|
||||||
groupby_field: 'code'
|
// groupby_field: 'code'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var query = builder.build();
|
// var query = builder.build();
|
||||||
|
//
|
||||||
it('should generate correct query', function() {
|
// it('should generate correct query', function() {
|
||||||
expect(query).to.be('select code, mean(value) from "google.test" where $timeFilter and code=1 ' +
|
// expect(query).to.be('select code, mean(value) from "google.test" where $timeFilter and code=1 ' +
|
||||||
'group by time($interval), code order asc');
|
// 'group by time($interval), code order asc');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should expose groupByFiled', function() {
|
// it('should expose groupByFiled', function() {
|
||||||
expect(builder.groupByField).to.be('code');
|
// expect(builder.groupByField).to.be('code');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('series with fill and minimum group by time', function() {
|
// describe('series with fill and minimum group by time', function() {
|
||||||
var builder = new InfluxQueryBuilder({
|
// var builder = new InfluxQueryBuilder({
|
||||||
series: 'google.test',
|
// series: 'google.test',
|
||||||
column: 'value',
|
// column: 'value',
|
||||||
function: 'mean',
|
// function: 'mean',
|
||||||
fill: '0',
|
// fill: '0',
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var query = builder.build();
|
// var query = builder.build();
|
||||||
|
//
|
||||||
it('should generate correct query', function() {
|
// it('should generate correct query', function() {
|
||||||
expect(query).to.be('select mean(value) from "google.test" where $timeFilter ' +
|
// expect(query).to.be('select mean(value) from "google.test" where $timeFilter ' +
|
||||||
'group by time($interval) fill(0) order asc');
|
// 'group by time($interval) fill(0) order asc');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('merge function detection', function() {
|
// describe('merge function detection', function() {
|
||||||
it('should not quote wrap regex merged series', function() {
|
// it('should not quote wrap regex merged series', function() {
|
||||||
var builder = new InfluxQueryBuilder({
|
// var builder = new InfluxQueryBuilder({
|
||||||
series: 'merge(/^google.test/)',
|
// series: 'merge(/^google.test/)',
|
||||||
column: 'value',
|
// column: 'value',
|
||||||
function: 'mean'
|
// function: 'mean'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var query = builder.build();
|
// var query = builder.build();
|
||||||
|
//
|
||||||
expect(query).to.be('select mean(value) from merge(/^google.test/) where $timeFilter ' +
|
// expect(query).to.be('select mean(value) from merge(/^google.test/) where $timeFilter ' +
|
||||||
'group by time($interval) order asc');
|
// 'group by time($interval) order asc');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should quote wrap series names that start with "merge"', function() {
|
// it('should quote wrap series names that start with "merge"', function() {
|
||||||
var builder = new InfluxQueryBuilder({
|
// var builder = new InfluxQueryBuilder({
|
||||||
series: 'merge.google.test',
|
// series: 'merge.google.test',
|
||||||
column: 'value',
|
// column: 'value',
|
||||||
function: 'mean'
|
// function: 'mean'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var query = builder.build();
|
// var query = builder.build();
|
||||||
|
//
|
||||||
expect(query).to.be('select mean(value) from "merge.google.test" where $timeFilter ' +
|
// expect(query).to.be('select mean(value) from "merge.google.test" where $timeFilter ' +
|
||||||
'group by time($interval) order asc');
|
// 'group by time($interval) order asc');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,220 +1,220 @@
|
|||||||
define([
|
define([
|
||||||
'features/influxdb/influxSeries'
|
'features/influxdb/influxSeries'
|
||||||
], function(InfluxSeries) {
|
], function(/*InfluxSeries*/) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('when generating timeseries from influxdb response', function() {
|
// describe('when generating timeseries from influxdb response', function() {
|
||||||
|
//
|
||||||
describe('given two series', function() {
|
// describe('given two series', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'sequence_number'],
|
// columns: ['time', 'mean', 'sequence_number'],
|
||||||
name: 'prod.server1.cpu',
|
// name: 'prod.server1.cpu',
|
||||||
points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
// points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'sequence_number'],
|
// columns: ['time', 'mean', 'sequence_number'],
|
||||||
name: 'prod.server2.cpu',
|
// name: 'prod.server2.cpu',
|
||||||
points: [[1402596000, 15, 1], [1402596001, 16, 2]]
|
// points: [[1402596000, 15, 1], [1402596001, 16, 2]]
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate two time series', function() {
|
// it('should generate two time series', function() {
|
||||||
expect(result.length).to.be(2);
|
// expect(result.length).to.be(2);
|
||||||
expect(result[0].target).to.be('prod.server1.cpu.mean');
|
// expect(result[0].target).to.be('prod.server1.cpu.mean');
|
||||||
expect(result[0].datapoints[0][0]).to.be(10);
|
// expect(result[0].datapoints[0][0]).to.be(10);
|
||||||
expect(result[0].datapoints[0][1]).to.be(1402596000);
|
// expect(result[0].datapoints[0][1]).to.be(1402596000);
|
||||||
expect(result[0].datapoints[1][0]).to.be(12);
|
// expect(result[0].datapoints[1][0]).to.be(12);
|
||||||
expect(result[0].datapoints[1][1]).to.be(1402596001);
|
// expect(result[0].datapoints[1][1]).to.be(1402596001);
|
||||||
|
//
|
||||||
expect(result[1].target).to.be('prod.server2.cpu.mean');
|
// expect(result[1].target).to.be('prod.server2.cpu.mean');
|
||||||
expect(result[1].datapoints[0][0]).to.be(15);
|
// expect(result[1].datapoints[0][0]).to.be(15);
|
||||||
expect(result[1].datapoints[0][1]).to.be(1402596000);
|
// expect(result[1].datapoints[0][1]).to.be(1402596000);
|
||||||
expect(result[1].datapoints[1][0]).to.be(16);
|
// expect(result[1].datapoints[1][0]).to.be(16);
|
||||||
expect(result[1].datapoints[1][1]).to.be(1402596001);
|
// expect(result[1].datapoints[1][1]).to.be(1402596001);
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('given an alias format', function() {
|
// describe('given an alias format', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'sequence_number'],
|
// columns: ['time', 'mean', 'sequence_number'],
|
||||||
name: 'prod.server1.cpu',
|
// name: 'prod.server1.cpu',
|
||||||
points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
// points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
alias: '$s.testing'
|
// alias: '$s.testing'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate correct series name', function() {
|
// it('should generate correct series name', function() {
|
||||||
expect(result[0].target).to.be('prod.server1.cpu.testing');
|
// expect(result[0].target).to.be('prod.server1.cpu.testing');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('given an alias format with segment numbers', function() {
|
// describe('given an alias format with segment numbers', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'sequence_number'],
|
// columns: ['time', 'mean', 'sequence_number'],
|
||||||
name: 'prod.server1.cpu',
|
// name: 'prod.server1.cpu',
|
||||||
points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
// points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
alias: '$1.mean'
|
// alias: '$1.mean'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate correct series name', function() {
|
// it('should generate correct series name', function() {
|
||||||
expect(result[0].target).to.be('server1.mean');
|
// expect(result[0].target).to.be('server1.mean');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('given an alias format and many segments', function() {
|
// describe('given an alias format and many segments', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'sequence_number'],
|
// columns: ['time', 'mean', 'sequence_number'],
|
||||||
name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12',
|
// name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12',
|
||||||
points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
// points: [[1402596000, 10, 1], [1402596001, 12, 2]]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
alias: '$5.$11.mean'
|
// alias: '$5.$11.mean'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate correct series name', function() {
|
// it('should generate correct series name', function() {
|
||||||
expect(result[0].target).to.be('a5.a11.mean');
|
// expect(result[0].target).to.be('a5.a11.mean');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
|
//
|
||||||
describe('given an alias format with group by field', function() {
|
// describe('given an alias format with group by field', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'host'],
|
// columns: ['time', 'mean', 'host'],
|
||||||
name: 'prod.cpu',
|
// name: 'prod.cpu',
|
||||||
points: [[1402596000, 10, 'A']]
|
// points: [[1402596000, 10, 'A']]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
groupByField: 'host',
|
// groupByField: 'host',
|
||||||
alias: '$g.$1'
|
// alias: '$g.$1'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate correct series name', function() {
|
// it('should generate correct series name', function() {
|
||||||
expect(result[0].target).to.be('A.cpu');
|
// expect(result[0].target).to.be('A.cpu');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('given group by column', function() {
|
// describe('given group by column', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'mean', 'host'],
|
// columns: ['time', 'mean', 'host'],
|
||||||
name: 'prod.cpu',
|
// name: 'prod.cpu',
|
||||||
points: [
|
// points: [
|
||||||
[1402596000, 10, 'A'],
|
// [1402596000, 10, 'A'],
|
||||||
[1402596001, 11, 'A'],
|
// [1402596001, 11, 'A'],
|
||||||
[1402596000, 5, 'B'],
|
// [1402596000, 5, 'B'],
|
||||||
[1402596001, 6, 'B'],
|
// [1402596001, 6, 'B'],
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
groupByField: 'host'
|
// groupByField: 'host'
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getTimeSeries();
|
// var result = series.getTimeSeries();
|
||||||
|
//
|
||||||
it('should generate two time series', function() {
|
// it('should generate two time series', function() {
|
||||||
expect(result.length).to.be(2);
|
// expect(result.length).to.be(2);
|
||||||
expect(result[0].target).to.be('prod.cpu.A');
|
// expect(result[0].target).to.be('prod.cpu.A');
|
||||||
expect(result[0].datapoints[0][0]).to.be(10);
|
// expect(result[0].datapoints[0][0]).to.be(10);
|
||||||
expect(result[0].datapoints[0][1]).to.be(1402596000);
|
// expect(result[0].datapoints[0][1]).to.be(1402596000);
|
||||||
expect(result[0].datapoints[1][0]).to.be(11);
|
// expect(result[0].datapoints[1][0]).to.be(11);
|
||||||
expect(result[0].datapoints[1][1]).to.be(1402596001);
|
// expect(result[0].datapoints[1][1]).to.be(1402596001);
|
||||||
|
//
|
||||||
expect(result[1].target).to.be('prod.cpu.B');
|
// expect(result[1].target).to.be('prod.cpu.B');
|
||||||
expect(result[1].datapoints[0][0]).to.be(5);
|
// expect(result[1].datapoints[0][0]).to.be(5);
|
||||||
expect(result[1].datapoints[0][1]).to.be(1402596000);
|
// expect(result[1].datapoints[0][1]).to.be(1402596000);
|
||||||
expect(result[1].datapoints[1][0]).to.be(6);
|
// expect(result[1].datapoints[1][0]).to.be(6);
|
||||||
expect(result[1].datapoints[1][1]).to.be(1402596001);
|
// expect(result[1].datapoints[1][1]).to.be(1402596001);
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe("when creating annotations from influxdb response", function() {
|
// describe("when creating annotations from influxdb response", function() {
|
||||||
describe('given column mapping for all columns', function() {
|
// describe('given column mapping for all columns', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'text', 'sequence_number', 'title', 'tags'],
|
// columns: ['time', 'text', 'sequence_number', 'title', 'tags'],
|
||||||
name: 'events1',
|
// name: 'events1',
|
||||||
points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']]
|
// points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
annotation: {
|
// annotation: {
|
||||||
query: 'select',
|
// query: 'select',
|
||||||
titleColumn: 'title',
|
// titleColumn: 'title',
|
||||||
tagsColumn: 'tags',
|
// tagsColumn: 'tags',
|
||||||
textColumn: 'text',
|
// textColumn: 'text',
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getAnnotations();
|
// var result = series.getAnnotations();
|
||||||
|
//
|
||||||
it(' should generate 2 annnotations ', function() {
|
// it(' should generate 2 annnotations ', function() {
|
||||||
expect(result.length).to.be(2);
|
// expect(result.length).to.be(2);
|
||||||
expect(result[0].annotation.query).to.be('select');
|
// expect(result[0].annotation.query).to.be('select');
|
||||||
expect(result[0].title).to.be('Hello');
|
// expect(result[0].title).to.be('Hello');
|
||||||
expect(result[0].time).to.be(1402596000000);
|
// expect(result[0].time).to.be(1402596000000);
|
||||||
expect(result[0].tags).to.be('B');
|
// expect(result[0].tags).to.be('B');
|
||||||
expect(result[0].text).to.be('some text');
|
// expect(result[0].text).to.be('some text');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('given no column mapping', function() {
|
// describe('given no column mapping', function() {
|
||||||
var series = new InfluxSeries({
|
// var series = new InfluxSeries({
|
||||||
seriesList: [
|
// seriesList: [
|
||||||
{
|
// {
|
||||||
columns: ['time', 'text', 'sequence_number'],
|
// columns: ['time', 'text', 'sequence_number'],
|
||||||
name: 'events1',
|
// name: 'events1',
|
||||||
points: [[1402596000000, 'some text', 1]]
|
// points: [[1402596000000, 'some text', 1]]
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
annotation: { query: 'select' }
|
// annotation: { query: 'select' }
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
var result = series.getAnnotations();
|
// var result = series.getAnnotations();
|
||||||
|
//
|
||||||
it('should generate 1 annnotation', function() {
|
// it('should generate 1 annnotation', function() {
|
||||||
expect(result.length).to.be(1);
|
// expect(result.length).to.be(1);
|
||||||
expect(result[0].title).to.be('some text');
|
// expect(result[0].title).to.be('some text');
|
||||||
expect(result[0].time).to.be(1402596000000);
|
// expect(result[0].time).to.be(1402596000000);
|
||||||
expect(result[0].tags).to.be(undefined);
|
// expect(result[0].tags).to.be(undefined);
|
||||||
expect(result[0].text).to.be(undefined);
|
// expect(result[0].text).to.be(undefined);
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,100 +1,100 @@
|
|||||||
define([
|
define([
|
||||||
'helpers',
|
'helpers',
|
||||||
'features/influxdb/datasource'
|
'features/influxdb/datasource'
|
||||||
], function(helpers) {
|
], function(/*helpers*/) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('InfluxDatasource', function() {
|
// describe('InfluxDatasource', function() {
|
||||||
var ctx = new helpers.ServiceTestContext();
|
// var ctx = new helpers.ServiceTestContext();
|
||||||
|
//
|
||||||
beforeEach(module('grafana.services'));
|
// beforeEach(module('grafana.services'));
|
||||||
beforeEach(ctx.providePhase(['templateSrv']));
|
// beforeEach(ctx.providePhase(['templateSrv']));
|
||||||
beforeEach(ctx.createService('InfluxDatasource'));
|
// beforeEach(ctx.createService('InfluxDatasource'));
|
||||||
beforeEach(function() {
|
// beforeEach(function() {
|
||||||
ctx.ds = new ctx.service({ url: '', user: 'test', password: 'mupp' });
|
// ctx.ds = new ctx.service({ url: '', user: 'test', password: 'mupp' });
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('When querying influxdb with one target using query editor target spec', function() {
|
// describe('When querying influxdb with one target using query editor target spec', function() {
|
||||||
var results;
|
// var results;
|
||||||
var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+
|
// var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+
|
||||||
"+where+time+%3E+now()-1h+group+by+time(1s)+order+asc";
|
// "+where+time+%3E+now()-1h+group+by+time(1s)+order+asc";
|
||||||
var query = {
|
// var query = {
|
||||||
range: { from: 'now-1h', to: 'now' },
|
// range: { from: 'now-1h', to: 'now' },
|
||||||
targets: [{ series: 'test', column: 'value', function: 'mean' }],
|
// targets: [{ series: 'test', column: 'value', function: 'mean' }],
|
||||||
interval: '1s'
|
// interval: '1s'
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
var response = [{
|
// var response = [{
|
||||||
columns: ["time", "sequence_nr", "value"],
|
// columns: ["time", "sequence_nr", "value"],
|
||||||
name: 'test',
|
// name: 'test',
|
||||||
points: [[10, 1, 1]],
|
// points: [[10, 1, 1]],
|
||||||
}];
|
// }];
|
||||||
|
//
|
||||||
beforeEach(function() {
|
// beforeEach(function() {
|
||||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
// ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||||
ctx.ds.query(query).then(function(data) { results = data; });
|
// ctx.ds.query(query).then(function(data) { results = data; });
|
||||||
ctx.$httpBackend.flush();
|
// ctx.$httpBackend.flush();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should generate the correct query', function() {
|
// it('should generate the correct query', function() {
|
||||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
// ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should return series list', function() {
|
// it('should return series list', function() {
|
||||||
expect(results.data.length).to.be(1);
|
// expect(results.data.length).to.be(1);
|
||||||
expect(results.data[0].target).to.be('test.value');
|
// expect(results.data[0].target).to.be('test.value');
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('When querying influxdb with one raw query', function() {
|
// describe('When querying influxdb with one raw query', function() {
|
||||||
var results;
|
// var results;
|
||||||
var urlExpected = "/series?p=mupp&q=select+value+from+series"+
|
// var urlExpected = "/series?p=mupp&q=select+value+from+series"+
|
||||||
"+where+time+%3E+now()-1h";
|
// "+where+time+%3E+now()-1h";
|
||||||
var query = {
|
// var query = {
|
||||||
range: { from: 'now-1h', to: 'now' },
|
// range: { from: 'now-1h', to: 'now' },
|
||||||
targets: [{ query: "select value from series where $timeFilter", rawQuery: true }]
|
// targets: [{ query: "select value from series where $timeFilter", rawQuery: true }]
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
var response = [];
|
// var response = [];
|
||||||
|
//
|
||||||
beforeEach(function() {
|
// beforeEach(function() {
|
||||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
// ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||||
ctx.ds.query(query).then(function(data) { results = data; });
|
// ctx.ds.query(query).then(function(data) { results = data; });
|
||||||
ctx.$httpBackend.flush();
|
// ctx.$httpBackend.flush();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should generate the correct query', function() {
|
// it('should generate the correct query', function() {
|
||||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
// ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
describe('When issuing annotation query', function() {
|
// describe('When issuing annotation query', function() {
|
||||||
var results;
|
// var results;
|
||||||
var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
|
// var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
|
||||||
"+where+time+%3E+now()-1h";
|
// "+where+time+%3E+now()-1h";
|
||||||
|
//
|
||||||
var range = { from: 'now-1h', to: 'now' };
|
// var range = { from: 'now-1h', to: 'now' };
|
||||||
var annotation = { query: 'select title from events.$server where $timeFilter' };
|
// var annotation = { query: 'select title from events.$server where $timeFilter' };
|
||||||
var response = [];
|
// var response = [];
|
||||||
|
//
|
||||||
beforeEach(function() {
|
// beforeEach(function() {
|
||||||
ctx.templateSrv.replace = function(str) {
|
// ctx.templateSrv.replace = function(str) {
|
||||||
return str.replace('$server', 'backend_01');
|
// return str.replace('$server', 'backend_01');
|
||||||
};
|
// };
|
||||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
// ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||||
ctx.ds.annotationQuery(annotation, range).then(function(data) { results = data; });
|
// ctx.ds.annotationQuery(annotation, range).then(function(data) { results = data; });
|
||||||
ctx.$httpBackend.flush();
|
// ctx.$httpBackend.flush();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
it('should generate the correct query', function() {
|
// it('should generate the correct query', function() {
|
||||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
// ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user