Explorar o código

feat: add ad hoc filters directly from table panel cells, kibana 3 style, #8052

Torkel Ödegaard %!s(int64=8) %!d(string=hai) anos
pai
achega
a5d5f3d82f

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

+ 3 - 3
public/app/plugins/datasource/elasticsearch/elastic_response.ts

@@ -100,9 +100,9 @@ ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, targe
   // add columns
   if (table.columns.length === 0) {
     for (let propKey of _.keys(props)) {
-      table.addColumn({text: propKey});
+      table.addColumn({text: propKey, filterable: true});
     }
-    table.addColumn({text: aggDef.field});
+    table.addColumn({text: aggDef.field, filterable: true});
   }
 
   // helper func to add values to value array
@@ -265,7 +265,7 @@ ElasticResponse.prototype.nameSeries = function(seriesList, target) {
 };
 
 ElasticResponse.prototype.processHits = function(hits, seriesList) {
-  var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total};
+  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++) {

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

@@ -389,7 +389,7 @@ describe('ElasticResponse', function() {
 
     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'}, {text: 'Count'}]);
+      expect(result.data[0].columns).to.eql([{text: 'bytes', filterable: true}, {text: 'Count'}]);
     });
   });
 

+ 17 - 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,25 @@ 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);
+      console.log('clicked', 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;