Parcourir la source

Merge branch 'elasticsearch-filtering'

Torkel Ödegaard il y a 8 ans
Parent
commit
781dd25c82

+ 9 - 0
public/app/core/table_model.ts

@@ -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;
+    }
+  }
 }

+ 3 - 3
public/app/features/dashboard/ad_hoc_filters.ts

@@ -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);
   }
 }
 

+ 1 - 4
public/app/features/dashboard/submenu/submenu.ts

@@ -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) {

+ 3 - 7
public/app/features/templating/editor_ctrl.ts

@@ -55,9 +55,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 +113,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() {
@@ -150,9 +148,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);
     };
   }
 }

+ 4 - 1
public/app/features/templating/specs/variable_srv_specs.ts

@@ -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();
       });

+ 39 - 5
public/app/features/templating/variable_srv.ts

@@ -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);

+ 11 - 2
public/app/plugins/datasource/elasticsearch/datasource.js

@@ -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

+ 0 - 2
public/app/plugins/datasource/elasticsearch/elastic_response.d.ts

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

+ 0 - 350
public/app/plugins/datasource/elasticsearch/elastic_response.js

@@ -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;
-});

+ 360 - 0
public/app/plugins/datasource/elasticsearch/elastic_response.ts

@@ -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 };
+};
+

+ 5 - 1
public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts

@@ -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',

+ 15 - 17
public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts

@@ -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].datapoints[1].host).to.be("server-2");
-      expect(result.data[0].datapoints[1].Average).to.be(2000);
+      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].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);
     });
   });
 

+ 9 - 1
public/app/plugins/panel/table/editor.html

@@ -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>
 

+ 23 - 1
public/app/plugins/panel/table/editor.ts

@@ -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();
   }
 

+ 16 - 1
public/app/plugins/panel/table/module.ts

@@ -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();
     });
 

+ 24 - 9
public/app/plugins/panel/table/renderer.ts

@@ -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(' ') + '"';
     }

+ 10 - 2
public/app/plugins/panel/table/transformers.ts

@@ -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) {

+ 14 - 0
public/sass/components/_panel_table.scss

@@ -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;