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

pull/6108/head
Torkel Ödegaard 9 years ago
parent 0a44add6c9
commit 83b9db51e3
  1. 4
      public/app/core/utils/kbn.js
  2. 159
      public/app/features/dashboard/ad_hoc_filters.ts
  3. 1
      public/app/features/dashboard/all.js
  4. 12
      public/app/features/dashboard/submenu/submenu.html
  5. 2
      public/app/features/templating/editorCtrl.js
  6. 13
      public/app/features/templating/partials/editor.html
  7. 50
      public/app/plugins/datasource/influxdb/datasource.ts
  8. 19
      public/app/plugins/datasource/influxdb/influx_query.ts
  9. 9
      public/app/plugins/datasource/prometheus/datasource.ts
  10. 1
      public/sass/components/_gf-form.scss
  11. 15
      public/sass/components/_submenu.scss

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save