Explorar el Código

feat(templating): great progress on adhoc filters, #6038

Torkel Ödegaard hace 9 años
padre
commit
83b9db51e3

+ 4 - 0
public/app/core/utils/kbn.js

@@ -9,6 +9,10 @@ function($, _, moment) {
   var kbn = {};
   kbn.valueFormats = {};
 
+  kbn.regexEscape = function(value) {
+    return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
+  };
+
   ///// HELPER FUNCTIONS /////
 
   kbn.round_interval = function(interval) {

+ 159 - 0
public/app/features/dashboard/ad_hoc_filters.ts

@@ -0,0 +1,159 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import angular from 'angular';
+import coreModule from 'app/core/core_module';
+
+export class AdHocFiltersCtrl {
+  segments: any;
+  variable: any;
+  removeTagFilterSegment: any;
+
+  /** @ngInject */
+  constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) {
+    this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'});
+    this.buildSegmentModel();
+  }
+
+  buildSegmentModel() {
+    this.segments = [];
+
+    if (this.variable.value && !_.isArray(this.variable.value)) {
+    }
+
+    for (let tag of this.variable.value) {
+      if (this.segments.length > 0) {
+        this.segments.push(this.uiSegmentSrv.newCondition('AND'));
+      }
+
+      if (tag.key !== undefined && tag.value !== undefined) {
+        this.segments.push(this.uiSegmentSrv.newKey(tag.key));
+        this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
+        this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
+      }
+    }
+
+    this.segments.push(this.uiSegmentSrv.newPlusButton());
+  }
+
+  getOptions(segment, index) {
+    if (segment.type === 'operator') {
+      return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>', '=~', '!~']));
+    }
+
+    return this.datasourceSrv.get(this.variable.datasource).then(ds => {
+      var options: any = {};
+      var promise = null;
+
+      if (segment.type !== 'value') {
+        promise = ds.getTagKeys();
+      } else {
+        options.key = this.segments[index-2].value;
+        promise = ds.getTagValues(options);
+      }
+
+      return promise.then(results => {
+        results = _.map(results, segment => {
+          return this.uiSegmentSrv.newSegment({value: segment.text});
+        });
+
+        // add remove option for keys
+        if (segment.type === 'key') {
+          results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
+        }
+        return results;
+      });
+    });
+  }
+
+  segmentChanged(segment, index) {
+    this.segments[index] = segment;
+
+    // handle remove tag condition
+    if (segment.value === this.removeTagFilterSegment.value) {
+      this.segments.splice(index, 3);
+      if (this.segments.length === 0) {
+        this.segments.push(this.uiSegmentSrv.newPlusButton());
+      } else if (this.segments.length > 2) {
+        this.segments.splice(Math.max(index-1, 0), 1);
+        if (this.segments[this.segments.length-1].type !== 'plus-button') {
+          this.segments.push(this.uiSegmentSrv.newPlusButton());
+        }
+      }
+    } else {
+      if (segment.type === 'plus-button') {
+        if (index > 2) {
+          this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
+        }
+        this.segments.push(this.uiSegmentSrv.newOperator('='));
+        this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
+        segment.type = 'key';
+        segment.cssClass = 'query-segment-key';
+      }
+
+      if ((index+1) === this.segments.length) {
+        this.segments.push(this.uiSegmentSrv.newPlusButton());
+      }
+    }
+
+    this.updateVariableModel();
+  }
+
+  updateVariableModel() {
+    var tags = [];
+    var tagIndex = -1;
+    var tagOperator = "";
+
+    this.segments.forEach((segment, index) => {
+      if (segment.fake) {
+        return;
+      }
+
+      switch (segment.type) {
+        case 'key': {
+          tags.push({key: segment.value});
+          tagIndex += 1;
+          break;
+        }
+        case 'value': {
+          tags[tagIndex].value = segment.value;
+          break;
+        }
+        case 'operator': {
+          tags[tagIndex].operator = segment.value;
+          break;
+        }
+        case 'condition': {
+          break;
+        }
+      }
+    });
+
+    this.$rootScope.$broadcast('refresh');
+    this.variable.value = tags;
+  }
+}
+
+var template = `
+<div class="gf-form-inline">
+  <div class="gf-form" ng-repeat="segment in ctrl.segments">
+    <metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
+                    on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
+  </div>
+</div>
+`;
+
+export function adHocFiltersComponent() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: AdHocFiltersCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      variable: "="
+    }
+  };
+}
+
+coreModule.directive('adHocFilters', adHocFiltersComponent);

+ 1 - 0
public/app/features/dashboard/all.js

@@ -20,4 +20,5 @@ define([
   './import/dash_import',
   './export/export_modal',
   './dash_list_ctrl',
+  './ad_hoc_filters',
 ], function () {});

+ 3 - 9
public/app/features/dashboard/submenu/submenu.html

@@ -1,19 +1,13 @@
 <div class="submenu-controls gf-form-query">
 	<ul ng-if="ctrl.dashboard.templating.list.length > 0">
-		<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item">
+		<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
 			<div class="gf-form">
-				<label class="gf-form-label template-variable " ng-hide="variable.hide === 1">
+				<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
 					{{variable.label || variable.name}}:
 				</label>
 				<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
 			</div>
-			<span ng-if="variable.type === 'adhoc'">
-				<div class="gf-form">
-					<label class="gf-form-label">hostname</label>
-					<label class="gf-form-label query-operator">=</label>
-					<label class="gf-form-label">server1</label>
-				</div>
-			</span>
+			<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
 		</li>
 	</ul>
 

+ 1 - 1
public/app/features/templating/editorCtrl.js

@@ -56,7 +56,7 @@ function (angular, _) {
       $scope.datasourceTypes = {};
       $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
         $scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
-        return !ds.meta.builtIn;
+        return !ds.meta.builtIn && ds.value !== null;
       });
 
       $scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {

+ 12 - 1
public/app/features/templating/partials/editor.html

@@ -165,7 +165,7 @@
 
         <div class="gf-form-inline">
           <div class="gf-form max-width-21">
-            <span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
+            <span class="gf-form-label width-7">Data source</span>
             <div class="gf-form-select-wrapper max-width-14">
               <select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
             </div>
@@ -233,6 +233,17 @@
         </div>
       </div>
 
+			<div ng-show="current.type === 'adhoc'" class="gf-form-group">
+        <h5 class="section-heading">Options</h5>
+
+				<div class="gf-form max-width-21">
+					<span class="gf-form-label width-8">Data source</span>
+					<div class="gf-form-select-wrapper max-width-14">
+						<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+					</div>
+				</div>
+      </div>
+
       <div class="section gf-form-group" ng-show="showSelectionOptions()">
         <h5 class="section-heading">Selection Options</h5>
         <div class="section">

+ 37 - 13
public/app/plugins/datasource/influxdb/datasource.ts

@@ -7,6 +7,7 @@ import * as dateMath from 'app/core/utils/datemath';
 import InfluxSeries from './influx_series';
 import InfluxQuery from './influx_query';
 import ResponseParser from './response_parser';
+import InfluxQueryBuilder from './query_builder';
 
 export default class InfluxDatasource {
   type: string;
@@ -43,18 +44,35 @@ export default class InfluxDatasource {
 
   query(options) {
     var timeFilter = this.getTimeFilter(options);
+    var scopedVars = _.extend({}, options.scopedVars);
+    var targets = _.cloneDeep(options.targets);
     var queryTargets = [];
     var i, y;
 
-    var allQueries = _.map(options.targets, (target) => {
+    var allQueries = _.map(targets, target => {
       if (target.hide) { return ""; }
 
+      if (!target.rawQuery) {
+        // apply add hoc filters
+        for (let variable of this.templateSrv.variables) {
+          if (variable.type === 'adhoc' && variable.datasource === this.name) {
+            for (let tag of variable.value) {
+              if (tag.key !== undefined && tag.value !== undefined) {
+                target.tags.push({key: tag.key, value: tag.value, condition: 'AND'});
+              }
+            }
+          }
+        }
+      }
+
       queryTargets.push(target);
 
       // build query
-      var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars);
+      scopedVars.interval = {value: target.interval || options.interval};
+
+      var queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
       var query =  queryModel.render(true);
-      query = query.replace(/\$interval/g, (target.interval || options.interval));
+
       return query;
     }).reduce((acc, current) => {
       if (current !== "") {
@@ -64,10 +82,10 @@ export default class InfluxDatasource {
     });
 
     // replace grafana variables
-    allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
+    scopedVars.timeFilter = {value: timeFilter};
 
     // replace templated variables
-    allQueries = this.templateSrv.replace(allQueries, options.scopedVars);
+    allQueries = this.templateSrv.replace(allQueries, scopedVars);
 
     return this._seriesQuery(allQueries).then((data): any => {
       if (!data || !data.results) {
@@ -124,16 +142,23 @@ export default class InfluxDatasource {
   };
 
   metricFindQuery(query) {
-    var interpolated;
-    try {
-      interpolated = this.templateSrv.replace(query, null, 'regex');
-    } catch (err) {
-      return this.$q.reject(err);
-    }
+    var interpolated = this.templateSrv.replace(query, null, 'regex');
 
     return this._seriesQuery(interpolated)
       .then(_.curry(this.responseParser.parse)(query));
-  };
+  }
+
+  getTagKeys(options) {
+    var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
+    var query = queryBuilder.buildExploreQuery('TAG_KEYS');
+    return this.metricFindQuery(query);
+  }
+
+  getTagValues(options) {
+    var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
+    var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
+    return this.metricFindQuery(query);
+  }
 
   _seriesQuery(query) {
     if (!query) { return this.$q.when({results: []}); }
@@ -141,7 +166,6 @@ export default class InfluxDatasource {
     return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
   }
 
-
   serializeParams(params) {
     if (!params) { return '';}
 

+ 17 - 2
public/app/plugins/datasource/influxdb/influx_query.ts

@@ -2,6 +2,7 @@
 
 import _ from 'lodash';
 import queryPart from './query_part';
+import kbn from 'app/core/utils/kbn';
 
 export default class InfluxQuery {
   target: any;
@@ -155,7 +156,7 @@ export default class InfluxQuery {
       if (operator !== '>' && operator !== '<') {
         value = "'" + value.replace(/\\/g, '\\\\') + "'";
       }
-    } else if (interpolate){
+    } else if (interpolate) {
       value = this.templateSrv.replace(value, this.scopedVars, 'regex');
     }
 
@@ -181,12 +182,26 @@ export default class InfluxQuery {
     return policy + measurement;
   }
 
+  interpolateQueryStr(value, variable, defaultFormatFn) {
+    // if no multi or include all do not regexEscape
+    if (!variable.multi && !variable.includeAll) {
+      return value;
+    }
+
+    if (typeof value === 'string') {
+      return kbn.regexEscape(value);
+    }
+
+    var escapedValues = _.map(value, kbn.regexEscape);
+    return escapedValues.join('|');
+  };
+
   render(interpolate?) {
     var target = this.target;
 
     if (target.rawQuery) {
       if (interpolate) {
-        return this.templateSrv.replace(target.query, this.scopedVars, 'regex');
+        return this.templateSrv.replace(target.query, this.scopedVars, this.interpolateQueryStr);
       } else {
         return target.query;
       }

+ 3 - 6
public/app/plugins/datasource/prometheus/datasource.ts

@@ -3,6 +3,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import moment from 'moment';
+import kbn from 'app/core/utils/kbn';
 
 import * as dateMath from 'app/core/utils/datemath';
 import PrometheusMetricFindQuery from './metric_find_query';
@@ -40,10 +41,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
     return backendSrv.datasourceRequest(options);
   };
 
-  function regexEscape(value) {
-    return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
-  }
-
   this.interpolateQueryExpr = function(value, variable, defaultFormatFn) {
     // if no multi or include all do not regexEscape
     if (!variable.multi && !variable.includeAll) {
@@ -51,10 +48,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
     }
 
     if (typeof value === 'string') {
-      return regexEscape(value);
+      return kbn.regexEscape(value);
     }
 
-    var escapedValues = _.map(value, regexEscape);
+    var escapedValues = _.map(value, kbn.regexEscape);
     return escapedValues.join('|');
   };
 

+ 0 - 1
public/sass/components/_gf-form.scss

@@ -48,7 +48,6 @@ $gf-form-margin: 0.25rem;
 .gf-form-label {
   padding: $input-padding-y $input-padding-x;
   margin-right: $gf-form-margin;
-  line-height: $input-line-height;
   flex-shrink: 0;
 
   background-color: $input-label-bg;

+ 2 - 13
public/sass/components/_submenu.scss

@@ -1,6 +1,5 @@
 .submenu-controls {
   margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
-  font-size: 16px;
 }
 
 .annotation-disabled, .annotation-disabled a {
@@ -25,12 +24,12 @@
   .fa-caret-down {
     font-size: 75%;
     position: relative;
-    top: 1px;
+    top: -1px;
+    left: 1px;
   }
 }
 
 .variable-value-link {
-  font-size: 16px;
   padding-right: 10px;
   .label-tag {
     margin: 0 5px;
@@ -39,19 +38,9 @@
   padding: 8px 7px;
   box-sizing: content-box;
   display: inline-block;
-  font-weight: normal;
-  display: inline-block;
   color: $text-color;
 }
 
-.submenu-item-label {
-  padding: 8px 0px 8px 7px;
-  box-sizing: content-box;
-  display: inline-block;
-  font-weight: normal;
-  display: inline-block;
-}
-
 .variable-link-wrapper  {
   display: inline-block;
   position: relative;