mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(templating): great progress on adhoc filters, #6038
This commit is contained in:
@@ -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
public/app/features/dashboard/ad_hoc_filters.ts
Normal file
159
public/app/features/dashboard/ad_hoc_filters.ts
Normal file
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user