Merge branch 'master' into ace-editor

This commit is contained in:
Torkel Ödegaard
2017-08-09 10:51:31 +02:00
134 changed files with 5760 additions and 4582 deletions

View File

@@ -129,6 +129,10 @@ function (angular, _, coreModule, config) {
}
var first = variable.current.value;
if (first === 'default') {
first = config.defaultDatasource;
}
var ds = config.datasources[first];
if (ds) {

View File

@@ -3,9 +3,11 @@ export default class TableModel {
columns: any[];
rows: any[];
type: string;
columnMap: any;
constructor() {
this.columns = [];
this.columnMap = {};
this.rows = [];
this.type = 'table';
}
@@ -36,4 +38,11 @@ export default class TableModel {
this.columns[options.col].desc = false;
}
}
addColumn(col) {
if (!this.columnMap[col.text]) {
this.columns.push(col);
this.columnMap[col.text] = col;
}
}
}

View File

@@ -15,39 +15,43 @@
<tbody>
<tr>
<td>Total dashboards</td>
<td>{{ctrl.stats.dashboard_count}}</td>
<td>{{ctrl.stats.dashboards}}</td>
</tr>
<tr>
<td>Total users</td>
<td>{{ctrl.stats.user_count}}</td>
<td>{{ctrl.stats.users}}</td>
</tr>
<tr>
<td>Active users (seen last 14 days)</td>
<td>{{ctrl.stats.activeUsers}}</td>
</tr>
<tr>
<td>Total organizations</td>
<td>{{ctrl.stats.org_count}}</td>
<td>{{ctrl.stats.orgs}}</td>
</tr>
<tr>
<td>Total datasources</td>
<td>{{ctrl.stats.data_source_count}}</td>
<td>{{ctrl.stats.datasources}}</td>
</tr>
<tr>
<td>Total playlists</td>
<td>{{ctrl.stats.playlist_count}}</td>
<td>{{ctrl.stats.playlists}}</td>
</tr>
<tr>
<td>Total snapshots</td>
<td>{{ctrl.stats.db_snapshot_count}}</td>
<td>{{ctrl.stats.snapshots}}</td>
</tr>
<tr>
<td>Total dashboard tags</td>
<td>{{ctrl.stats.db_tag_count}}</td>
<td>{{ctrl.stats.tags}}</td>
</tr>
<tr>
<td>Total starred dashboards</td>
<td>{{ctrl.stats.starred_db_count}}</td>
<td>{{ctrl.stats.stars}}</td>
</tr>
<tr>
<td>Total alerts</td>
<td>{{ctrl.stats.alert_count}}</td>
<td>{{ctrl.stats.alerts}}</td>
</tr>
</tbody>
</table>

View File

@@ -25,7 +25,11 @@
<th>Name</th>
<th>Login</th>
<th>Email</th>
<th style="white-space: nowrap">Grafana Admin</th>
<th>
Seen
<tip>Time since user was seen using Grafana</tip>
</th>
<th></th>
<th></th>
</tr>
</thead>
@@ -35,7 +39,12 @@
<td>{{user.name}}</td>
<td>{{user.login}}</td>
<td>{{user.email}}</td>
<td>{{user.isAdmin}}</td>
<td>
{{user.lastSeenAtAge}}
</td>
<td>
<i class="fa fa-shield" ng-show="user.isAdmin" bs-tooltip="'Grafana Admin'"></i>
</td>
<td class="text-right">
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
<i class="fa fa-edit"></i>

View File

@@ -10,9 +10,10 @@ export class AdHocFiltersCtrl {
removeTagFilterSegment: any;
/** @ngInject */
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) {
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, private $scope, private $rootScope) {
this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'});
this.buildSegmentModel();
this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope);
}
buildSegmentModel() {
@@ -141,8 +142,7 @@ export class AdHocFiltersCtrl {
}
this.variable.setFilters(filters);
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
this.variableSrv.variableUpdated(this.variable, true);
}
}

View File

@@ -135,9 +135,10 @@ export class DashNavCtrl {
viewJson() {
var clone = this.dashboard.getSaveModelClone();
var html = angular.toJson(clone, true);
var uri = "data:application/json;charset=utf-8," + encodeURIComponent(html);
var newWindow = window.open(uri);
this.$rootScope.appEvent('show-json-editor', {
object: clone,
});
}
showSearch() {

View File

@@ -44,12 +44,16 @@ export class SaveDashboardAsModalCtrl {
this.clone.editable = true;
this.clone.hideControls = false;
// remove alerts
this.clone.rows.forEach(row => {
row.panels.forEach(panel => {
delete panel.alert;
// remove alerts if source dashboard is already persisted
// do not want to create alert dupes
if (dashboard.id > 0) {
this.clone.rows.forEach(row => {
row.panels.forEach(panel => {
delete panel.thresholds;
delete panel.alert;
});
});
});
}
delete this.clone.autoUpdate;
}

View File

@@ -22,10 +22,7 @@ export class SubmenuCtrl {
}
variableUpdated(variable) {
this.variableSrv.variableUpdated(variable).then(() => {
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});
this.variableSrv.variableUpdated(variable, true);
}
openEditView(editview) {

View File

@@ -41,6 +41,10 @@
<tr>
<th>Login</th>
<th>Email</th>
<th>
Seen
<tip>Time since user was seen using Grafana</tip>
</th>
<th>Role</th>
<th style="width: 34px;"></th>
</tr>
@@ -48,6 +52,7 @@
<tr ng-repeat="user in ctrl.users">
<td>{{user.login}}</td>
<td><span class="ellipsis">{{user.email}}</span></td>
<td>{{user.lastSeenAtAge}}</td>
<td>
<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
</select>

View File

@@ -35,14 +35,6 @@ export class VariableEditorCtrl {
$scope.init = function() {
$scope.mode = 'list';
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
return !ds.meta.mixed && ds.value !== null;
});
$scope.datasourceTypes = _($scope.datasources).uniqBy('meta.id').map(function(ds) {
return {text: ds.meta.name, value: ds.meta.id};
}).value();
$scope.variables = variableSrv.variables;
$scope.reset();
@@ -55,9 +47,8 @@ export class VariableEditorCtrl {
$scope.add = function() {
if ($scope.isValid()) {
$scope.variables.push($scope.current);
variableSrv.addVariable($scope.current);
$scope.update();
$scope.dashboard.updateSubmenuVisibility();
}
};
@@ -114,9 +105,8 @@ export class VariableEditorCtrl {
$scope.duplicate = function(variable) {
var clone = _.cloneDeep(variable.getSaveModel());
$scope.current = variableSrv.createVariableFromModel(clone);
$scope.variables.push($scope.current);
$scope.current.name = 'copy_of_'+variable.name;
$scope.dashboard.updateSubmenuVisibility();
$scope.variableSrv.addVariable($scope.current);
};
$scope.update = function() {
@@ -132,6 +122,15 @@ export class VariableEditorCtrl {
$scope.reset = function() {
$scope.currentIsNew = true;
$scope.current = variableSrv.createVariableFromModel({type: 'query'});
// this is done here in case a new data source type variable was added
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
return !ds.meta.mixed && ds.value !== null;
});
$scope.datasourceTypes = _($scope.datasources).uniqBy('meta.id').map(function(ds) {
return {text: ds.meta.name, value: ds.meta.id};
}).value();
};
$scope.typeChanged = function() {
@@ -150,9 +149,7 @@ export class VariableEditorCtrl {
};
$scope.removeVariable = function(variable) {
var index = _.indexOf($scope.variables, variable);
$scope.variables.splice(index, 1);
$scope.dashboard.updateSubmenuVisibility();
variableSrv.removeVariable(variable);
};
}
}

View File

@@ -22,6 +22,7 @@ describe('VariableSrv', function() {
ctx.variableSrv.init({
templating: {list: []},
events: new Emitter(),
updateSubmenuVisibility: sinon.stub(),
});
ctx.$rootScope.$digest();
}));
@@ -41,7 +42,9 @@ describe('VariableSrv', function() {
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel);
scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
ctx.variableSrv.addVariable(scenario.variable);
ctx.variableSrv.updateOptions(scenario.variable);
ctx.$rootScope.$digest();
});

View File

@@ -90,17 +90,24 @@ export class VariableSrv {
return variable;
}
addVariable(model) {
var variable = this.createVariableFromModel(model);
addVariable(variable) {
this.variables.push(variable);
return variable;
this.templateSrv.updateTemplateData();
this.dashboard.updateSubmenuVisibility();
}
removeVariable(variable) {
var index = _.indexOf(this.variables, variable);
this.variables.splice(index, 1);
this.templateSrv.updateTemplateData();
this.dashboard.updateSubmenuVisibility();
}
updateOptions(variable) {
return variable.updateOptions();
}
variableUpdated(variable) {
variableUpdated(variable, emitChangeEvents?) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (variable.initLock) {
return this.$q.when();
@@ -117,7 +124,12 @@ export class VariableSrv {
}
});
return this.$q.all(promises);
return this.$q.all(promises).then(() => {
if (emitChangeEvents) {
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
}
});
}
selectOptionsForCurrentValue(variable) {
@@ -218,6 +230,28 @@ export class VariableSrv {
// update url
this.$location.search(params);
}
setAdhocFilter(options) {
var variable = _.find(this.variables, {type: 'adhoc', datasource: options.datasource});
if (!variable) {
variable = this.createVariableFromModel({name: 'Filters', type: 'adhoc', datasource: options.datasource});
this.addVariable(variable);
}
let filters = variable.filters;
let filter = _.find(filters, {key: options.key, value: options.value});
if (!filter) {
filter = {key: options.key, value: options.value};
filters.push(filter);
}
filter.operator = options.operator;
variable.setFilters(filters);
this.variableUpdated(variable, true);
}
}
coreModule.service('variableSrv', VariableSrv);

View File

@@ -11,6 +11,8 @@ define([
function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) {
'use strict';
ElasticResponse = ElasticResponse.ElasticResponse;
/** @ngInject */
function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
this.basicAuth = instanceSettings.basicAuth;
@@ -270,10 +272,17 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var subObj = obj[key];
// Check mapping field for nested fields
if (subObj.hasOwnProperty('properties')) {
if (_.isObject(subObj.properties)) {
fieldNameParts.push(key);
getFieldsRecursively(subObj.properties);
} else {
}
if (_.isObject(subObj.fields)) {
fieldNameParts.push(key);
getFieldsRecursively(subObj.fields);
}
if (_.isString(subObj.type)) {
var fieldName = fieldNameParts.concat(key).join('.');
// Hide meta-fields and check field type

View File

@@ -1,2 +0,0 @@
declare var test: any;
export default test;

View File

@@ -1,350 +0,0 @@
define([
"lodash",
"./query_def"
],
function (_, queryDef) {
'use strict';
function ElasticResponse(targets, response) {
this.targets = targets;
this.response = response;
}
ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) {
var metric, y, i, newSeries, bucket, value;
for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y];
if (metric.hide) {
continue;
}
switch(metric.type) {
case 'count': {
newSeries = { datapoints: [], metric: 'count', props: props};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
value = bucket.doc_count;
newSeries.datapoints.push([value, bucket.key]);
}
seriesList.push(newSeries);
break;
}
case 'percentiles': {
if (esAgg.buckets.length === 0) {
break;
}
var firstBucket = esAgg.buckets[0];
var percentiles = firstBucket[metric.id].values;
for (var percentileName in percentiles) {
newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
var values = bucket[metric.id].values;
newSeries.datapoints.push([values[percentileName], bucket.key]);
}
seriesList.push(newSeries);
}
break;
}
case 'extended_stats': {
for (var statName in metric.meta) {
if (!metric.meta[statName]) {
continue;
}
newSeries = {datapoints: [], metric: statName, props: props, field: metric.field};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
var stats = bucket[metric.id];
// add stats that are in nested obj to top level obj
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
newSeries.datapoints.push([stats[statName], bucket.key]);
}
seriesList.push(newSeries);
}
break;
}
default: {
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
value = bucket[metric.id];
if (value !== undefined) {
if (value.normalized_value) {
newSeries.datapoints.push([value.normalized_value, bucket.key]);
} else {
newSeries.datapoints.push([value.value, bucket.key]);
}
}
}
seriesList.push(newSeries);
break;
}
}
}
};
ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, docs, props) {
var metric, y, i, bucket, metricName, doc;
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
doc = _.defaults({}, props);
doc[aggDef.field] = bucket.key;
for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y];
switch(metric.type) {
case "count": {
metricName = this._getMetricName(metric.type);
doc[metricName] = bucket.doc_count;
break;
}
case 'extended_stats': {
for (var statName in metric.meta) {
if (!metric.meta[statName]) {
continue;
}
var stats = bucket[metric.id];
// add stats that are in nested obj to top level obj
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
metricName = this._getMetricName(statName);
doc[metricName] = stats[statName];
}
break;
}
default: {
metricName = this._getMetricName(metric.type);
var otherMetrics = _.filter(target.metrics, {type: metric.type});
// if more of the same metric type include field field name in property
if (otherMetrics.length > 1) {
metricName += ' ' + metric.field;
}
doc[metricName] = bucket[metric.id].value;
break;
}
}
}
docs.push(doc);
}
};
// This is quite complex
// neeed to recurise down the nested buckets to build series
ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, docs, props, depth) {
var bucket, aggDef, esAgg, aggId;
var maxDepth = target.bucketAggs.length-1;
for (aggId in aggs) {
aggDef = _.find(target.bucketAggs, {id: aggId});
esAgg = aggs[aggId];
if (!aggDef) {
continue;
}
if (depth === maxDepth) {
if (aggDef.type === 'date_histogram') {
this.processMetrics(esAgg, target, seriesList, props);
} else {
this.processAggregationDocs(esAgg, aggDef, target, docs, props);
}
} else {
for (var nameIndex in esAgg.buckets) {
bucket = esAgg.buckets[nameIndex];
props = _.clone(props);
if (bucket.key !== void 0) {
props[aggDef.field] = bucket.key;
} else {
props["filter"] = nameIndex;
}
if (bucket.key_as_string) {
props[aggDef.field] = bucket.key_as_string;
}
this.processBuckets(bucket, target, seriesList, docs, props, depth+1);
}
}
}
};
ElasticResponse.prototype._getMetricName = function(metric) {
var metricDef = _.find(queryDef.metricAggTypes, {value: metric});
if (!metricDef) {
metricDef = _.find(queryDef.extendedStats, {value: metric});
}
return metricDef ? metricDef.text : metric;
};
ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) {
var metricName = this._getMetricName(series.metric);
if (target.alias) {
var regex = /\{\{([\s\S]+?)\}\}/g;
return target.alias.replace(regex, function(match, g1, g2) {
var group = g1 || g2;
if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; }
if (series.props[group] !== void 0) { return series.props[group]; }
if (group === 'metric') { return metricName; }
if (group === 'field') { return series.field; }
return match;
});
}
if (series.field && queryDef.isPipelineAgg(series.metric)) {
var appliedAgg = _.find(target.metrics, { id: series.field });
if (appliedAgg) {
metricName += ' ' + queryDef.describeMetric(appliedAgg);
} else {
metricName = 'Unset';
}
} else if (series.field) {
metricName += ' ' + series.field;
}
var propKeys = _.keys(series.props);
if (propKeys.length === 0) {
return metricName;
}
var name = '';
for (var propName in series.props) {
name += series.props[propName] + ' ';
}
if (metricTypeCount === 1) {
return name.trim();
}
return name.trim() + ' ' + metricName;
};
ElasticResponse.prototype.nameSeries = function(seriesList, target) {
var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length;
var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length;
for (var i = 0; i < seriesList.length; i++) {
var series = seriesList[i];
series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount);
}
};
ElasticResponse.prototype.processHits = function(hits, seriesList) {
var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total};
var propName, hit, doc, i;
for (i = 0; i < hits.hits.length; i++) {
hit = hits.hits[i];
doc = {
_id: hit._id,
_type: hit._type,
_index: hit._index
};
if (hit._source) {
for (propName in hit._source) {
doc[propName] = hit._source[propName];
}
}
for (propName in hit.fields) {
doc[propName] = hit.fields[propName];
}
series.datapoints.push(doc);
}
seriesList.push(series);
};
ElasticResponse.prototype.trimDatapoints = function(aggregations, target) {
var histogram = _.find(target.bucketAggs, { type: 'date_histogram'});
var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges;
if (shouldDropFirstAndLast) {
var trim = histogram.settings.trimEdges;
for(var prop in aggregations) {
var points = aggregations[prop];
if (points.datapoints.length > trim * 2) {
points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim);
}
}
}
};
ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) {
var result = {};
result.data = JSON.stringify(err, null, 4);
if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) {
result.message = err.root_cause[0].reason;
} else {
result.message = err.reason || 'Unkown elatic error response';
}
if (response.$$config) {
result.config = response.$$config;
}
return result;
};
ElasticResponse.prototype.getTimeSeries = function() {
var seriesList = [];
for (var i = 0; i < this.response.responses.length; i++) {
var response = this.response.responses[i];
if (response.error) {
throw this.getErrorFromElasticResponse(this.response, response.error);
}
if (response.hits && response.hits.hits.length > 0) {
this.processHits(response.hits, seriesList);
}
if (response.aggregations) {
var aggregations = response.aggregations;
var target = this.targets[i];
var tmpSeriesList = [];
var docs = [];
this.processBuckets(aggregations, target, tmpSeriesList, docs, {}, 0);
this.trimDatapoints(tmpSeriesList, target);
this.nameSeries(tmpSeriesList, target);
for (var y = 0; y < tmpSeriesList.length; y++) {
seriesList.push(tmpSeriesList[y]);
}
if (seriesList.length === 0 && docs.length > 0) {
seriesList.push({target: 'docs', type: 'docs', datapoints: docs});
}
}
}
return { data: seriesList };
};
return ElasticResponse;
});

View File

@@ -0,0 +1,360 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import queryDef from "./query_def";
import TableModel from 'app/core/table_model';
export function ElasticResponse(targets, response) {
this.targets = targets;
this.response = response;
}
ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) {
var metric, y, i, newSeries, bucket, value;
for (y = 0; y < target.metrics.length; y++) {
metric = target.metrics[y];
if (metric.hide) {
continue;
}
switch (metric.type) {
case 'count': {
newSeries = { datapoints: [], metric: 'count', props: props};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
value = bucket.doc_count;
newSeries.datapoints.push([value, bucket.key]);
}
seriesList.push(newSeries);
break;
}
case 'percentiles': {
if (esAgg.buckets.length === 0) {
break;
}
var firstBucket = esAgg.buckets[0];
var percentiles = firstBucket[metric.id].values;
for (var percentileName in percentiles) {
newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
var values = bucket[metric.id].values;
newSeries.datapoints.push([values[percentileName], bucket.key]);
}
seriesList.push(newSeries);
}
break;
}
case 'extended_stats': {
for (var statName in metric.meta) {
if (!metric.meta[statName]) {
continue;
}
newSeries = {datapoints: [], metric: statName, props: props, field: metric.field};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
var stats = bucket[metric.id];
// add stats that are in nested obj to top level obj
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
newSeries.datapoints.push([stats[statName], bucket.key]);
}
seriesList.push(newSeries);
}
break;
}
default: {
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
for (i = 0; i < esAgg.buckets.length; i++) {
bucket = esAgg.buckets[i];
value = bucket[metric.id];
if (value !== undefined) {
if (value.normalized_value) {
newSeries.datapoints.push([value.normalized_value, bucket.key]);
} else {
newSeries.datapoints.push([value.value, bucket.key]);
}
}
}
seriesList.push(newSeries);
break;
}
}
}
};
ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, table, props) {
// add columns
if (table.columns.length === 0) {
for (let propKey of _.keys(props)) {
table.addColumn({text: propKey, filterable: true});
}
table.addColumn({text: aggDef.field, filterable: true});
}
// helper func to add values to value array
let addMetricValue = (values, metricName, value) => {
table.addColumn({text: metricName});
values.push(value);
};
for (let bucket of esAgg.buckets) {
let values = [];
for (let propValues of _.values(props)) {
values.push(propValues);
}
// add bucket key (value)
values.push(bucket.key);
for (let metric of target.metrics) {
switch (metric.type) {
case "count": {
addMetricValue(values, this._getMetricName(metric.type), bucket.doc_count);
break;
}
case 'extended_stats': {
for (var statName in metric.meta) {
if (!metric.meta[statName]) {
continue;
}
var stats = bucket[metric.id];
// add stats that are in nested obj to top level obj
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
addMetricValue(values, this._getMetricName(statName), stats[statName]);
}
break;
}
default: {
let metricName = this._getMetricName(metric.type);
let otherMetrics = _.filter(target.metrics, {type: metric.type});
// if more of the same metric type include field field name in property
if (otherMetrics.length > 1) {
metricName += ' ' + metric.field;
}
addMetricValue(values, metricName, bucket[metric.id].value);
break;
}
}
}
table.rows.push(values);
}
};
// This is quite complex
// neeed to recurise down the nested buckets to build series
ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, table, props, depth) {
var bucket, aggDef, esAgg, aggId;
var maxDepth = target.bucketAggs.length-1;
for (aggId in aggs) {
aggDef = _.find(target.bucketAggs, {id: aggId});
esAgg = aggs[aggId];
if (!aggDef) {
continue;
}
if (depth === maxDepth) {
if (aggDef.type === 'date_histogram') {
this.processMetrics(esAgg, target, seriesList, props);
} else {
this.processAggregationDocs(esAgg, aggDef, target, table, props);
}
} else {
for (var nameIndex in esAgg.buckets) {
bucket = esAgg.buckets[nameIndex];
props = _.clone(props);
if (bucket.key !== void 0) {
props[aggDef.field] = bucket.key;
} else {
props["filter"] = nameIndex;
}
if (bucket.key_as_string) {
props[aggDef.field] = bucket.key_as_string;
}
this.processBuckets(bucket, target, seriesList, table, props, depth+1);
}
}
}
};
ElasticResponse.prototype._getMetricName = function(metric) {
var metricDef = _.find(queryDef.metricAggTypes, {value: metric});
if (!metricDef) {
metricDef = _.find(queryDef.extendedStats, {value: metric});
}
return metricDef ? metricDef.text : metric;
};
ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) {
var metricName = this._getMetricName(series.metric);
if (target.alias) {
var regex = /\{\{([\s\S]+?)\}\}/g;
return target.alias.replace(regex, function(match, g1, g2) {
var group = g1 || g2;
if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; }
if (series.props[group] !== void 0) { return series.props[group]; }
if (group === 'metric') { return metricName; }
if (group === 'field') { return series.field; }
return match;
});
}
if (series.field && queryDef.isPipelineAgg(series.metric)) {
var appliedAgg = _.find(target.metrics, { id: series.field });
if (appliedAgg) {
metricName += ' ' + queryDef.describeMetric(appliedAgg);
} else {
metricName = 'Unset';
}
} else if (series.field) {
metricName += ' ' + series.field;
}
var propKeys = _.keys(series.props);
if (propKeys.length === 0) {
return metricName;
}
var name = '';
for (var propName in series.props) {
name += series.props[propName] + ' ';
}
if (metricTypeCount === 1) {
return name.trim();
}
return name.trim() + ' ' + metricName;
};
ElasticResponse.prototype.nameSeries = function(seriesList, target) {
var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length;
var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length;
for (var i = 0; i < seriesList.length; i++) {
var series = seriesList[i];
series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount);
}
};
ElasticResponse.prototype.processHits = function(hits, seriesList) {
var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total, filterable: true};
var propName, hit, doc, i;
for (i = 0; i < hits.hits.length; i++) {
hit = hits.hits[i];
doc = {
_id: hit._id,
_type: hit._type,
_index: hit._index
};
if (hit._source) {
for (propName in hit._source) {
doc[propName] = hit._source[propName];
}
}
for (propName in hit.fields) {
doc[propName] = hit.fields[propName];
}
series.datapoints.push(doc);
}
seriesList.push(series);
};
ElasticResponse.prototype.trimDatapoints = function(aggregations, target) {
var histogram = _.find(target.bucketAggs, { type: 'date_histogram'});
var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges;
if (shouldDropFirstAndLast) {
var trim = histogram.settings.trimEdges;
for (var prop in aggregations) {
var points = aggregations[prop];
if (points.datapoints.length > trim * 2) {
points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim);
}
}
}
};
ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) {
var result: any = {};
result.data = JSON.stringify(err, null, 4);
if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) {
result.message = err.root_cause[0].reason;
} else {
result.message = err.reason || 'Unkown elatic error response';
}
if (response.$$config) {
result.config = response.$$config;
}
return result;
};
ElasticResponse.prototype.getTimeSeries = function() {
var seriesList = [];
for (var i = 0; i < this.response.responses.length; i++) {
var response = this.response.responses[i];
if (response.error) {
throw this.getErrorFromElasticResponse(this.response, response.error);
}
if (response.hits && response.hits.hits.length > 0) {
this.processHits(response.hits, seriesList);
}
if (response.aggregations) {
var aggregations = response.aggregations;
var target = this.targets[i];
var tmpSeriesList = [];
var table = new TableModel();
this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0);
this.trimDatapoints(tmpSeriesList, target);
this.nameSeries(tmpSeriesList, target);
for (var y = 0; y < tmpSeriesList.length; y++) {
seriesList.push(tmpSeriesList[y]);
}
if (table.rows.length > 0) {
seriesList.push(table);
}
}
}
return { data: seriesList };
};

View File

@@ -129,7 +129,10 @@ describe('ElasticDatasource', function() {
'@timestamp': {type: 'date'},
beat: {
properties: {
name: {type: 'string'},
name: {
fields: {raw: {type: 'keyword'}},
type: 'string'
},
hostname: {type: 'string'},
}
},
@@ -169,6 +172,7 @@ describe('ElasticDatasource', function() {
var fields = _.map(fieldObjects, 'text');
expect(fields).to.eql([
'@timestamp',
'beat.name.raw',
'beat.name',
'beat.hostname',
'system.cpu.system',

View File

@@ -1,6 +1,6 @@
import {describe, beforeEach, it, expect} from 'test/lib/common';
import ElasticResponse from '../elastic_response';
import {ElasticResponse} from '../elastic_response';
describe('ElasticResponse', function() {
var targets;
@@ -387,10 +387,9 @@ describe('ElasticResponse', function() {
result = new ElasticResponse(targets, response).getTimeSeries();
});
it('should return docs with byte and count', function() {
expect(result.data[0].datapoints.length).to.be(3);
expect(result.data[0].datapoints[0].Count).to.be(1);
expect(result.data[0].datapoints[0].bytes).to.be(1000);
it('should return table with byte and count', function() {
expect(result.data[0].rows.length).to.be(3);
expect(result.data[0].columns).to.eql([{text: 'bytes', filterable: true}, {text: 'Count'}]);
});
});
@@ -530,14 +529,14 @@ describe('ElasticResponse', function() {
it('should return table', function() {
expect(result.data.length).to.be(1);
expect(result.data[0].type).to.be('docs');
expect(result.data[0].datapoints.length).to.be(2);
expect(result.data[0].datapoints[0].host).to.be("server-1");
expect(result.data[0].datapoints[0].Average).to.be(1000);
expect(result.data[0].datapoints[0].Count).to.be(369);
expect(result.data[0].type).to.be('table');
expect(result.data[0].rows.length).to.be(2);
expect(result.data[0].rows[0][0]).to.be("server-1");
expect(result.data[0].rows[0][1]).to.be(1000);
expect(result.data[0].rows[0][2]).to.be(369);
expect(result.data[0].datapoints[1].host).to.be("server-2");
expect(result.data[0].datapoints[1].Average).to.be(2000);
expect(result.data[0].rows[1][0]).to.be("server-2");
expect(result.data[0].rows[1][1]).to.be(2000);
});
});
@@ -573,10 +572,9 @@ describe('ElasticResponse', function() {
});
it('should include field in metric name', function() {
expect(result.data[0].type).to.be('docs');
expect(result.data[0].datapoints[0].Average).to.be(undefined);
expect(result.data[0].datapoints[0]['Average test']).to.be(1000);
expect(result.data[0].datapoints[0]['Average test2']).to.be(3000);
expect(result.data[0].type).to.be('table');
expect(result.data[0].rows[0][1]).to.be(1000);
expect(result.data[0].rows[0][2]).to.be(3000);
});
});

View File

@@ -20,9 +20,10 @@ function (angular, _, $, gfunc) {
return {
link: function($scope, elem) {
var categories = gfunc.getCategories();
var allFunctions = getAllFunctionNames(categories);
var ctrl = $scope.ctrl;
var graphiteVersion = ctrl.datasource.graphiteVersion;
var categories = gfunc.getCategories(graphiteVersion);
var allFunctions = getAllFunctionNames(categories);
$scope.functionMenu = createFunctionDropDownMenu(categories);
@@ -94,14 +95,16 @@ function (angular, _, $, gfunc) {
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
var submenu = _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
};
});
return {
text: category,
submenu: _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
};
})
submenu: submenu
};
});
}

View File

@@ -0,0 +1,20 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class GraphiteConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
current: any;
/** @ngInject */
constructor($scope) {
this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
}
graphiteVersions = [
{name: '0.9.x', value: '0.9'},
{name: '1.0.x', value: '1.0'},
];
}

View File

@@ -11,6 +11,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9';
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
@@ -175,8 +176,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
};
if (options && options.range) {
httpOptions.params.from = this.translateTime(options.range.raw.from, false);
httpOptions.params.until = this.translateTime(options.range.raw.to, true);
httpOptions.params.from = this.translateTime(options.range.from, false);
httpOptions.params.until = this.translateTime(options.range.to, true);
}
return this.doGraphiteRequest(httpOptions).then(results => {

View File

@@ -115,27 +115,6 @@ function (_, $) {
category: categories.Combine,
});
addFuncDef({
name: 'mapSeries',
shortName: 'map',
params: [{ name: "node", type: 'int' }],
defaultParams: [3],
category: categories.Combine,
});
addFuncDef({
name: 'reduceSeries',
shortName: 'reduce',
params: [
{ name: "function", type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
{ name: "reduceNode", type: 'int', options: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] },
{ name: "reduceMatchers", type: 'string' },
{ name: "reduceMatchers", type: 'string' },
],
defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
category: categories.Combine,
});
addFuncDef({
name: 'sumSeries',
shortName: 'sum',
@@ -152,11 +131,6 @@ function (_, $) {
defaultParams: [''],
});
addFuncDef({
name: 'isNonNull',
category: categories.Combine,
});
addFuncDef({
name: 'rangeOfSeries',
category: categories.Combine
@@ -262,23 +236,6 @@ function (_, $) {
defaultParams: [3, "sum"]
});
addFuncDef({
name: "groupByNodes",
category: categories.Special,
params: [
{
name: "function",
type: "string",
options: ['sum', 'avg', 'maxSeries']
},
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
],
defaultParams: ["sum", 3]
});
addFuncDef({
name: 'aliasByNode',
category: categories.Special,
@@ -381,11 +338,6 @@ function (_, $) {
defaultParams: [10]
});
addFuncDef({
name: 'offsetToZero',
category: categories.Transform,
});
addFuncDef({
name: 'transformNull',
category: categories.Transform,
@@ -542,13 +494,6 @@ function (_, $) {
defaultParams: ['exclude']
});
addFuncDef({
name: "grep",
category: categories.Filter,
params: [{ name: "grep", type: 'string' }],
defaultParams: ['grep']
});
addFuncDef({
name: 'highestCurrent',
category: categories.Filter,
@@ -577,16 +522,6 @@ function (_, $) {
defaultParams: [10]
});
addFuncDef({
name: 'weightedAverage',
category: categories.Filter,
params: [
{ name: 'other', type: 'value_or_series', optional: true },
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
],
defaultParams: ['#A', 4]
});
addFuncDef({
name: 'movingMedian',
category: categories.Filter,
@@ -643,11 +578,6 @@ function (_, $) {
defaultParams: [5]
});
addFuncDef({
name: 'removeEmptySeries',
category: categories.Filter
});
addFuncDef({
name: 'useSeriesAbove',
category: categories.Filter,
@@ -659,6 +589,239 @@ function (_, $) {
defaultParams: [0, 'search', 'replace']
});
////////////////////
// Graphite 1.0.x //
////////////////////
addFuncDef({
name: 'aggregateLine',
category: categories.Combine,
params: [{ name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last']}],
defaultParams: ['avg'],
version: '1.0'
});
addFuncDef({
name: 'averageOutsidePercentile',
category: categories.Filter,
params: [{ name: "n", type: "int", }],
defaultParams: [95],
version: '1.0'
});
addFuncDef({
name: 'delay',
category: categories.Transform,
params: [{ name: 'steps', type: 'int', }],
defaultParams: [1],
version: '1.0'
});
addFuncDef({
name: 'exponentialMovingAverage',
category: categories.Calculate,
params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
defaultParams: [10],
version: '1.0'
});
addFuncDef({
name: 'fallbackSeries',
category: categories.Special,
params: [{ name: 'fallback', type: 'string' }],
defaultParams: ['constantLine(0)'],
version: '1.0'
});
addFuncDef({
name: "grep",
category: categories.Filter,
params: [{ name: "grep", type: 'string' }],
defaultParams: ['grep'],
version: '1.0'
});
addFuncDef({
name: "groupByNodes",
category: categories.Special,
params: [
{
name: "function",
type: "string",
options: ['sum', 'avg', 'maxSeries']
},
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
],
defaultParams: ["sum", 3],
version: '1.0'
});
addFuncDef({
name: 'integralByInterval',
category: categories.Transform,
params: [{ name: "intervalUnit", type: "select", options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] }],
defaultParams: ['1d'],
version: '1.0'
});
addFuncDef({
name: 'interpolate',
category: categories.Transform,
params: [{ name: 'limit', type: 'int', optional: true}],
defaultParams: [],
version: '1.0'
});
addFuncDef({
name: 'invert',
category: categories.Transform,
version: '1.0'
});
addFuncDef({
name: 'isNonNull',
category: categories.Combine,
version: '1.0'
});
addFuncDef({
name: 'linearRegression',
category: categories.Calculate,
params: [
{ name: "startSourceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true },
{ name: "endSourceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true }
],
defaultParams: [],
version: '1.0'
});
addFuncDef({
name: 'mapSeries',
shortName: 'map',
params: [{ name: "node", type: 'int' }],
defaultParams: [3],
category: categories.Combine,
version: '1.0'
});
addFuncDef({
name: 'movingMin',
category: categories.Calculate,
params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
defaultParams: [10],
version: '1.0'
});
addFuncDef({
name: 'movingMax',
category: categories.Calculate,
params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
defaultParams: [10],
version: '1.0'
});
addFuncDef({
name: 'movingSum',
category: categories.Calculate,
params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
defaultParams: [10],
version: '1.0'
});
addFuncDef({
name: "multiplySeriesWithWildcards",
category: categories.Calculate,
params: [
{ name: "position", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
{ name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
{ name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
],
defaultParams: [2],
version: '1.0'
});
addFuncDef({
name: 'offsetToZero',
category: categories.Transform,
version: '1.0'
});
addFuncDef({
name: 'pow',
category: categories.Transform,
params: [{ name: 'factor', type: 'int' }],
defaultParams: [10],
version: '1.0'
});
addFuncDef({
name: 'powSeries',
category: categories.Transform,
params: optionalSeriesRefArgs,
defaultParams: [''],
version: '1.0'
});
addFuncDef({
name: 'reduceSeries',
shortName: 'reduce',
params: [
{ name: "function", type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
{ name: "reduceNode", type: 'int', options: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] },
{ name: "reduceMatchers", type: 'string' },
{ name: "reduceMatchers", type: 'string' },
],
defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
category: categories.Combine,
version: '1.0'
});
addFuncDef({
name: 'removeBetweenPercentile',
category: categories.Filter,
params: [{ name: "n", type: "int", }],
defaultParams: [95],
version: '1.0'
});
addFuncDef({
name: 'removeEmptySeries',
category: categories.Filter,
version: '1.0'
});
addFuncDef({
name: 'squareRoot',
category: categories.Transform,
version: '1.0'
});
addFuncDef({
name: 'timeSlice',
category: categories.Transform,
params: [
{ name: "startSliceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d']},
{ name: "endSliceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true }
],
defaultParams: ['-1h'],
version: '1.0'
});
addFuncDef({
name: 'weightedAverage',
category: categories.Filter,
params: [
{ name: 'other', type: 'value_or_series', optional: true },
{ name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
],
defaultParams: ['#A', 4],
version: '1.0'
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
@@ -737,6 +900,16 @@ function (_, $) {
this.text = text;
};
function isVersionRelatedFunction(func, graphiteVersion) {
return isVersionGreaterOrEqual(graphiteVersion, func.version) || !func.version;
}
function isVersionGreaterOrEqual(a, b) {
var a_num = Number(a);
var b_num = Number(b);
return a_num >= b_num;
}
return {
createFuncInstance: function(funcDef, options) {
if (_.isString(funcDef)) {
@@ -752,8 +925,18 @@ function (_, $) {
return index[name];
},
getCategories: function() {
return categories;
getCategories: function(graphiteVersion) {
var filteredCategories = {};
_.each(categories, function(functions, category) {
var filteredFuncs = _.filter(functions, function(func) {
return isVersionRelatedFunction(func, graphiteVersion);
});
if (filteredFuncs.length) {
filteredCategories[category] = filteredFuncs;
}
});
return filteredCategories;
}
};

View File

@@ -1,9 +1,6 @@
import {GraphiteDatasource} from './datasource';
import {GraphiteQueryCtrl} from './query_ctrl';
class GraphiteConfigCtrl {
static templateUrl = 'partials/config.html';
}
import {GraphiteConfigCtrl} from './config_ctrl';
class GraphiteQueryOptionsCtrl {
static templateUrl = 'partials/query.options.html';

View File

@@ -3,3 +3,16 @@
suggest-url="http://localhost:8080">
</datasource-http-settings>
<h3 class="page-heading">Graphite details</h3>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-8">
Version
<info-popover mode="right-normal" position="top center">
This option controls what functions are available in the Graphite query editor.
</info-popover>
</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.graphiteVersion" ng-options="f.value as f.name for f in ctrl.graphiteVersions"></select>
</div>
</div>

View File

@@ -5,7 +5,7 @@ import {GraphiteDatasource} from "../datasource";
describe('graphiteDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings: any = {url: [''], name: 'graphiteProd'};
var instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));

View File

@@ -28,7 +28,14 @@ An annotation is an event that is overlayed on top of graphs. The query can have
Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &gt; from AND UNIX_TIMESTAMP(time_date_time) &lt; 1492750877
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &gt; 1492750877 AND UNIX_TIMESTAMP(time_date_time) &lt; 1492750877
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)
- $__timeTo() -&gt; FROM_UNIXTIME(1492750877)
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
</pre>
</div>
</div>

View File

@@ -54,7 +54,14 @@ Table:
Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &ge; from AND UNIX_TIMESTAMP(time_date_time) &le; 1492750877
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &ge; 1492750877 AND UNIX_TIMESTAMP(time_date_time) &le; 1492750877
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)
- $__timeTo() -&gt; FROM_UNIXTIME(1492750877)
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
</pre>
</div>

View File

@@ -0,0 +1,300 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import d3 from 'd3';
import {contextSrv} from 'app/core/core';
import {tickStep} from 'app/core/utils/ticks';
let module = angular.module('grafana.directives');
/**
* Color legend for heatmap editor.
*/
module.directive('colorLegend', function() {
return {
restrict: 'E',
template: '<div class="heatmap-color-legend"><svg width="16.8rem" height="24px"></svg></div>',
link: function(scope, elem, attrs) {
let ctrl = scope.ctrl;
let panel = scope.ctrl.panel;
render();
ctrl.events.on('render', function() {
render();
});
function render() {
let legendElem = $(elem).find('svg');
let legendWidth = Math.floor(legendElem.outerWidth());
if (panel.color.mode === 'spectrum') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
let colorScale = getColorScale(colorScheme, legendWidth);
drawSimpleColorLegend(elem, colorScale);
} else if (panel.color.mode === 'opacity') {
let colorOptions = panel.color;
drawSimpleOpacityLegend(elem, colorOptions);
}
}
}
};
});
/**
* Heatmap legend with scale values.
*/
module.directive('heatmapLegend', function() {
return {
restrict: 'E',
template: '<div class="heatmap-color-legend"><svg width="100px" height="14px"></svg></div>',
link: function(scope, elem, attrs) {
let ctrl = scope.ctrl;
let panel = scope.ctrl.panel;
render();
ctrl.events.on('render', function() {
render();
});
function render() {
clearLegend(elem);
if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
let rangeFrom = 0;
let rangeTo = ctrl.data.cardStats.max;
let maxValue = panel.color.max || rangeTo;
let minValue = panel.color.min || 0;
if (panel.color.mode === 'spectrum') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
} else if (panel.color.mode === 'opacity') {
let colorOptions = panel.color;
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
}
}
}
}
};
});
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
let legendHeight = legendElem.attr("height");
let rangeStep = 1;
if (rangeTo - rangeFrom > legendWidth) {
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
}
let widthFactor = legendWidth / (rangeTo - rangeFrom);
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
let colorScale = getColorScale(colorScheme, maxValue, minValue);
legend.selectAll(".heatmap-color-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d * widthFactor)
.attr("y", 0)
.attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => colorScale(d));
drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
}
function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
let legendHeight = legendElem.attr("height");
let rangeStep = 10;
let widthFactor = legendWidth / (rangeTo - rangeFrom);
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
let opacityScale = getOpacityScale(options, maxValue, minValue);
legend.selectAll(".heatmap-opacity-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d * widthFactor)
.attr("y", 0)
.attr("width", rangeStep * widthFactor)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", options.cardColor)
.style("opacity", d => opacityScale(d));
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
}
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) {
return;
}
let legendValueDomain = _.sortBy(colorScale.domain());
let legendValueScale = d3.scaleLinear()
.domain([0, rangeTo])
.range([0, legendWidth]);
let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
let xAxis = d3.axisBottom(legendValueScale)
.tickValues(ticks)
.tickSize(2);
let colorRect = legendElem.find(":first-child");
let posY = colorRect.height() + 2;
let posX = getSvgElemX(colorRect);
d3.select(legendElem.get(0)).append("g")
.attr("class", "axis")
.attr("transform", "translate(" + posX + "," + posY + ")")
.call(xAxis);
legend.select(".axis").select(".domain").remove();
}
function drawSimpleColorLegend(elem, colorScale) {
let legendElem = $(elem).find('svg');
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth());
let legendHeight = legendElem.attr("height");
if (legendWidth) {
let valuesNumber = Math.floor(legendWidth / 2);
let rangeStep = Math.floor(legendWidth / valuesNumber);
let valuesRange = d3.range(0, legendWidth, rangeStep);
let legend = d3.select(legendElem.get(0));
var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange);
legendRects.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => colorScale(d));
}
}
function drawSimpleOpacityLegend(elem, options) {
let legendElem = $(elem).find('svg');
clearLegend(elem);
let legend = d3.select(legendElem.get(0));
let legendWidth = Math.floor(legendElem.outerWidth());
let legendHeight = legendElem.attr("height");
if (legendWidth) {
let legendOpacityScale;
if (options.colorScale === 'linear') {
legendOpacityScale = d3.scaleLinear()
.domain([0, legendWidth])
.range([0, 1]);
} else if (options.colorScale === 'sqrt') {
legendOpacityScale = d3.scalePow().exponent(options.exponent)
.domain([0, legendWidth])
.range([0, 1]);
}
let rangeStep = 10;
let valuesRange = d3.range(0, legendWidth, rangeStep);
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
legendRects.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", options.cardColor)
.style("opacity", d => legendOpacityScale(d));
}
}
function clearLegend(elem) {
let legendElem = $(elem).find('svg');
legendElem.empty();
}
function getColorScale(colorScheme, maxValue, minValue = 0) {
let colorInterpolator = d3[colorScheme.value];
let colorScaleInverted = colorScheme.invert === 'always' ||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
let start = colorScaleInverted ? maxValue : minValue;
let end = colorScaleInverted ? minValue : maxValue;
return d3.scaleSequential(colorInterpolator).domain([start, end]);
}
function getOpacityScale(options, maxValue, minValue = 0) {
let legendOpacityScale;
if (options.colorScale === 'linear') {
legendOpacityScale = d3.scaleLinear()
.domain([minValue, maxValue])
.range([0, 1]);
} else if (options.colorScale === 'sqrt') {
legendOpacityScale = d3.scalePow().exponent(options.exponent)
.domain([minValue, maxValue])
.range([0, 1]);
}
return legendOpacityScale;
}
function getSvgElemX(elem) {
let svgElem = elem.get(0);
if (svgElem && svgElem.x && svgElem.x.baseVal) {
return elem.get(0).x.baseVal.value;
} else {
return 0;
}
}
function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
let range = rangeTo - rangeFrom;
let tickStepSize = tickStep(rangeFrom, rangeTo, 3);
let ticksNum = Math.round(range / tickStepSize);
let ticks = [];
for (let i = 0; i < ticksNum; i++) {
let current = tickStepSize * i;
// Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue);
continue;
} else if (minValue < current) {
ticks.push(minValue);
}
if (isValueCloseTo(maxValue, current, tickStepSize)) {
ticks.push(maxValue);
continue;
} else if (maxValue < current) {
ticks.push(maxValue);
}
ticks.push(tickStepSize * i);
}
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
ticks.push(maxValue);
}
ticks.push(rangeTo);
ticks = _.sortBy(_.uniq(ticks));
return ticks;
}
function isValueCloseTo(val, valueTo, step) {
let diff = Math.abs(val - valueTo);
return diff < step * 0.3;
}

View File

@@ -1,4 +1,10 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import d3 from 'd3';
import {contextSrv} from 'app/core/core';
const COLOR_LEGEND_SELECTOR = '.heatmap-color-legend';
export class HeatmapDisplayEditorCtrl {
panel: any;

View File

@@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series';
import {axesEditor} from './axes_editor';
import {heatmapDisplayEditor} from './display_editor';
import rendering from './rendering';
import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
let X_BUCKET_NUMBER_DEFAULT = 30;
let Y_BUCKET_NUMBER_DEFAULT = 10;
@@ -26,6 +26,9 @@ let panelDefaults = {
exponent: 0.5,
colorScheme: 'interpolateOranges',
},
legend: {
show: false
},
dataFormat: 'timeseries',
xAxis: {
show: true,
@@ -188,11 +191,15 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
yBucketSize = 1;
}
let {cards, cardStats} = convertToCards(bucketsData);
this.data = {
buckets: bucketsData,
heatmapStats: heatmapStats,
xBucketSize: xBucketSize,
yBucketSize: yBucketSize
yBucketSize: yBucketSize,
cards: cards,
cardStats: cardStats
};
}

View File

@@ -51,6 +51,7 @@ function elasticHistogramToHeatmap(seriesList) {
* @return {Array} Array of "card" objects
*/
function convertToCards(buckets) {
let min = 0, max = 0;
let cards = [];
_.forEach(buckets, xBucket => {
_.forEach(xBucket.buckets, yBucket=> {
@@ -62,10 +63,19 @@ function convertToCards(buckets) {
count: yBucket.count,
};
cards.push(card);
if (cards.length === 1) {
min = yBucket.count;
max = yBucket.count;
}
min = yBucket.count < min ? yBucket.count : min;
max = yBucket.count > max ? yBucket.count : max;
});
});
return cards;
let cardStats = {min, max};
return {cards, cardStats};
}
/**

View File

@@ -7,5 +7,8 @@
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div>
<div class="heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<heatmap-legend></heatmap-legend>
</div>
</div>
<div class="clearfix"></div>

View File

@@ -1,5 +1,5 @@
///<reference path="../../../headers/common.d.ts" />
import './color_legend';
import {HeatmapCtrl} from './heatmap_ctrl';
export {

View File

@@ -25,9 +25,6 @@
<label class="gf-form-label width-9">Exponent</label>
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<svg id="heatmap-opacity-legend" width="19em" height="2em"></svg>
</div>
</div>
<div ng-show="ctrl.panel.color.mode === 'spectrum'">
@@ -37,10 +34,31 @@
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
</div>
</div>
<div class="gf-form">
<svg id="heatmap-color-legend" width="19em" height="2em"></svg>
</div>
</div>
<div class="gf-form">
<color-legend></color-legend>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Color scale</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Min</label>
<input type="number" ng-model="ctrl.panel.color.min" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Max</label>
<input type="number" ng-model="ctrl.panel.color.max" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Legend</h5>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show legend"
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="section gf-form-group">

View File

@@ -8,7 +8,7 @@ import {appEvents, contextSrv} from 'app/core/core';
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
import d3 from 'd3';
import {HeatmapTooltip} from './heatmap_tooltip';
import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter';
import {mergeZeroBuckets} from './heatmap_data_converter';
let MIN_CARD_SIZE = 1,
CARD_PADDING = 1,
@@ -384,10 +384,12 @@ export default function link(scope, elem, attrs, ctrl) {
data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
}
let cardsData = convertToCards(data.buckets);
let maxValue = d3.max(cardsData, card => card.count);
let cardsData = data.cards;
let maxValueAuto = data.cardStats.max;
let maxValue = panel.color.max || maxValueAuto;
let minValue = panel.color.min || 0;
colorScale = getColorScale(maxValue);
colorScale = getColorScale(maxValue, minValue);
setOpacityScale(maxValue);
setCardSize();
@@ -434,14 +436,14 @@ export default function link(scope, elem, attrs, ctrl) {
.style("stroke-width", 0);
}
function getColorScale(maxValue) {
function getColorScale(maxValue, minValue = 0) {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
let colorInterpolator = d3[colorScheme.value];
let colorScaleInverted = colorScheme.invert === 'always' ||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
let start = colorScaleInverted ? maxValue : 0;
let end = colorScaleInverted ? 0 : maxValue;
let start = colorScaleInverted ? maxValue : minValue;
let end = colorScaleInverted ? minValue : maxValue;
return d3.scaleSequential(colorInterpolator).domain([start, end]);
}
@@ -704,78 +706,11 @@ export default function link(scope, elem, attrs, ctrl) {
}
}
function drawColorLegend() {
d3.select("#heatmap-color-legend").selectAll("rect").remove();
let legend = d3.select("#heatmap-color-legend");
let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth());
let legendHeight = d3.select("#heatmap-color-legend").attr("height");
let legendColorScale = getColorScale(legendWidth);
let rangeStep = 2;
let valuesRange = d3.range(0, legendWidth, rangeStep);
var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange);
legendRects.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => {
return legendColorScale(d);
});
}
function drawOpacityLegend() {
d3.select("#heatmap-opacity-legend").selectAll("rect").remove();
let legend = d3.select("#heatmap-opacity-legend");
let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth());
let legendHeight = d3.select("#heatmap-opacity-legend").attr("height");
let legendOpacityScale;
if (panel.color.colorScale === 'linear') {
legendOpacityScale = d3.scaleLinear()
.domain([0, legendWidth])
.range([0, 1]);
} else if (panel.color.colorScale === 'sqrt') {
legendOpacityScale = d3.scalePow().exponent(panel.color.exponent)
.domain([0, legendWidth])
.range([0, 1]);
}
let rangeStep = 1;
let valuesRange = d3.range(0, legendWidth, rangeStep);
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
legendRects.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", panel.color.cardColor)
.style("opacity", d => {
return legendOpacityScale(d);
});
}
function render() {
data = ctrl.data;
panel = ctrl.panel;
timeRange = ctrl.range;
// Draw only if color editor is opened
if (!d3.select("#heatmap-color-legend").empty()) {
drawColorLegend();
}
if (!d3.select("#heatmap-opacity-legend").empty()) {
drawOpacityLegend();
}
if (!setElementHeight() || !data) {
return;
}

View File

@@ -3,7 +3,8 @@
import _ from 'lodash';
import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
import TimeSeries from 'app/core/time_series2';
import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter';
import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap,
calculateBucketSize, isHeatmapDataEqual} from '../heatmap_data_converter';
describe('isHeatmapDataEqual', () => {
let ctx: any = {};
@@ -244,6 +245,47 @@ describe('ES Histogram converter', () => {
});
});
describe('convertToCards', () => {
let buckets = {};
beforeEach(() => {
buckets = {
'1422774000000': {
x: 1422774000000,
buckets: {
'1': { y: 1, values: [1], count: 1, bounds: {} },
'2': { y: 2, values: [2], count: 1, bounds: {} }
}
},
'1422774060000': {
x: 1422774060000,
buckets: {
'2': { y: 2, values: [2, 3], count: 2, bounds: {} }
}
},
};
});
it('should build proper cards data', () => {
let expectedCards = [
{x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {}},
{x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {}},
{x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {}}
];
let {cards, cardStats} = convertToCards(buckets);
expect(cards).to.eql(expectedCards);
});
it('should build proper cards stats', () => {
let expectedStats = {
min: 1,
max: 2
};
let {cards, cardStats} = convertToCards(buckets);
expect(cardStats).to.eql(expectedStats);
});
});
/**
* Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
* @param a

View File

@@ -11,8 +11,7 @@ import TimeSeries from 'app/core/time_series2';
import moment from 'moment';
import { Emitter } from 'app/core/core';
import rendering from '../rendering';
import { convertToHeatMap } from '../heatmap_data_converter';
// import d3 from 'd3';
import {convertToHeatMap, convertToCards} from '../heatmap_data_converter';
describe('grafanaHeatmap', function () {
@@ -115,8 +114,9 @@ describe('grafanaHeatmap', function () {
let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
ctx.data.buckets = bucketsData;
// console.log("bucketsData", bucketsData);
// console.log("series", ctrl.panel.yAxis.logBase, ctx.series.length);
let {cards, cardStats} = convertToCards(bucketsData);
ctx.data.cards = cards;
ctx.data.cardStats = cardStats;
let elemHtml = `
<div class="heatmap-wrapper">

View File

@@ -17,9 +17,17 @@
<span>{{column.text}}</span>
</label>
</div>
<div class="gf-form">
<div class="gf-form" ng-show="editor.canSetColumns">
<metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment>
</div>
<div class="gf-form" ng-hide="editor.canSetColumns">
<label class="gf-form-label">
Auto
<info-popover mode="right-normal" ng-if="editor.columnsHelpMessage">
{{editor.columnsHelpMessage}}
</info-popover>
</label>
</div>
</div>
</div>

View File

@@ -16,6 +16,8 @@ export class TablePanelEditorCtrl {
fontSizes: any;
addColumnSegment: any;
getColumnNames: any;
canSetColumns: boolean;
columnsHelpMessage: string;
/** @ngInject */
constructor($scope, private $q, private uiSegmentSrv) {
@@ -24,8 +26,27 @@ export class TablePanelEditorCtrl {
this.panel = this.panelCtrl.panel;
this.transformers = transformers;
this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
this.addColumnSegment = uiSegmentSrv.newPlusButton();
this.updateTransformHints();
}
updateTransformHints() {
this.canSetColumns = false;
this.columnsHelpMessage = '';
switch (this.panel.transform) {
case "timeseries_aggregations": {
this.canSetColumns = true;
break;
}
case "json": {
this.canSetColumns = true;
break;
}
case "table": {
this.columnsHelpMessage = "Columns and their order are determined by the data query";
}
}
}
getColumnOptions() {
@@ -57,6 +78,7 @@ export class TablePanelEditorCtrl {
this.panel.columns.push({text: 'Avg', value: 'avg'});
}
this.updateTransformHints();
this.render();
}

View File

@@ -50,8 +50,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
};
/** @ngInject */
constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize) {
constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize, private variableSrv) {
super($scope, $injector);
this.pageIndex = 0;
if (this.panel.styles === void 0) {
@@ -223,10 +224,24 @@ class TablePanelCtrl extends MetricsPanelCtrl {
selector: '[data-link-tooltip]'
});
function addFilterClicked(e) {
let filterData = $(e.currentTarget).data();
var options = {
datasource: panel.datasource,
key: data.columns[filterData.column].text,
value: data.rows[filterData.row][filterData.column],
operator: filterData.operator,
};
ctrl.variableSrv.setAdhocFilter(options);
}
elem.on('click', '.table-panel-page-link', switchPage);
elem.on('click', '.table-panel-filter-link', addFilterClicked);
var unbindDestroy = scope.$on('$destroy', function() {
elem.off('click', '.table-panel-page-link');
elem.off('click', '.table-panel-filter-link');
unbindDestroy();
});

View File

@@ -140,9 +140,12 @@ export class TableRenderer {
renderCell(columnIndex, rowIndex, value, addWidthHack = false) {
value = this.formatColumnValue(columnIndex, value);
var column = this.table.columns[columnIndex];
var style = '';
var cellClasses = [];
var cellClass = '';
if (this.colorState.cell) {
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
this.colorState.cell = null;
@@ -161,26 +164,25 @@ export class TableRenderer {
if (value === undefined) {
style = ' style="display:none;"';
this.table.columns[columnIndex].hidden = true;
column.hidden = true;
} else {
this.table.columns[columnIndex].hidden = false;
column.hidden = false;
}
var columnStyle = this.table.columns[columnIndex].style;
if (columnStyle && columnStyle.preserveFormat) {
if (column.style && column.style.preserveFormat) {
cellClasses.push("table-panel-cell-pre");
}
var columnHtml = value + widthHack;
var columnHtml = widthHack + value;
if (columnStyle && columnStyle.link) {
if (column.style && column.style.link) {
// Render cell as link
var scopedVars = this.renderRowVariables(rowIndex);
scopedVars['__cell'] = { value: value };
var cellLink = this.templateSrv.replace(columnStyle.linkUrl, scopedVars);
var cellLinkTooltip = this.templateSrv.replace(columnStyle.linkTooltip, scopedVars);
var cellTarget = columnStyle.linkTargetBlank ? '_blank' : '';
var cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars);
var cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars);
var cellTarget = column.style.linkTargetBlank ? '_blank' : '';
cellClasses.push("table-panel-cell-link");
columnHtml = `
@@ -190,6 +192,19 @@ export class TableRenderer {
`;
}
if (column.filterable) {
cellClasses.push("table-panel-cell-filterable");
columnHtml += `
<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter out value" data-placement="bottom"
data-row="${rowIndex}" data-column="${columnIndex}" data-operator="!=">
<i class="fa fa-search-minus"></i>
</a>
<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter for value" data-placement="bottom"
data-row="${rowIndex}" data-column="${columnIndex}" data-operator="=">
<i class="fa fa-search-plus"></i>
</a>`;
}
if (cellClasses.length) {
cellClass = ' class="' + cellClasses.join(' ') + '"';
}

View File

@@ -185,8 +185,16 @@ transformers['json'] = {
},
transform: function(data, panel, model) {
var i, y, z;
for (i = 0; i < panel.columns.length; i++) {
model.columns.push({text: panel.columns[i].text});
for (let column of panel.columns) {
var tableCol: any = {text: column.text};
// if filterable data then set columns to filterable
if (data.length > 0 && data[0].filterable) {
tableCol.filterable = true;
}
model.columns.push(tableCol);
}
if (model.columns.length === 0) {

View File

@@ -46,3 +46,46 @@
stroke-width: 1;
}
}
.heatmap-selection {
stroke-width: 1;
fill: rgba(102, 102, 102, 0.4);
stroke: rgba(102, 102, 102, 0.8);
}
.heatmap-legend-wrapper {
@include clearfix();
margin: 0 $spacer;
padding-top: 10px;
svg {
width: 100%;
max-width: 300px;
height: 33px;
float: left;
white-space: nowrap;
padding-left: 10px;
}
.heatmap-legend-values {
display: inline-block;
}
.axis .tick {
text {
fill: $text-color;
color: $text-color;
font-size: $font-size-sm;
}
line {
opacity: 0.4;
stroke: $text-color-weak;
}
.domain {
opacity: 0.4;
stroke: $text-color-weak;
}
}
}

View File

@@ -91,9 +91,23 @@
&.cell-highlighted:hover {
background-color: $tight-form-func-bg;
}
&:hover {
.table-panel-filter-link {
visibility: visible;
}
}
}
}
.table-panel-filter-link {
visibility: hidden;
color: $text-color-weak;
float: right;
display: block;
padding: 0 5px;
}
.table-panel-header-bg {
background: $grafanaListAccent;
border-top: 2px solid $body-bg;