Templating: Makes so __searchFilter can be used as part of regular expression (#20103)

* Fix: Fixes searchfilter wildcard char in regular expressions
Fixes: #20006

* Refactor: Uses TemplateSrv and ScopedVars instead

* Docs: Updates docs with new format

* Tests: Corrects test description
This commit is contained in:
Hugo Häggmark 2019-11-04 06:43:03 +01:00 committed by GitHub
parent fc5cc4cbf9
commit 7aeed4d987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 224 additions and 132 deletions

View File

@ -114,19 +114,25 @@ variable with all possible values that exist in the wildcard position.
You can also create nested variables that use other variables in their definition. For example
`apps.$app.servers.*` uses the variable `$app` in its query definition.
#### Using `$__searchFilter` to filter results in Query Variable
#### Using `__searchFilter` to filter results in Query Variable
> Available from Grafana 6.5 and above
Using `$__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `$__searchFilter` is `*`.
Using `__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `__searchFilter` is `*` and `` when used as part of a regular expression.
The example below shows how to use `$__searchFilter` as part of the query field to enable searching for `server` while the user types in the dropdown select box.
The example below shows how to use `__searchFilter` as part of the query field to enable searching for `server` while the user types in the dropdown select box.
Query
```bash
apps.$app.servers.$__searchFilter
```
TagValues
```bash
tag_values(server, server=~${__searchFilter:regex})
```
### Variable Usage
You can use a variable in a metric node path or as a parameter to a function.

View File

@ -276,17 +276,19 @@ the hosts variable only show hosts from the current selected region with a query
SELECT hostname FROM my_host WHERE region IN($region)
```
#### Using `$__searchFilter` to filter results in Query Variable
#### Using `__searchFilter` to filter results in Query Variable
> Available from Grafana 6.5 and above
Using `$__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `$__searchFilter` is `%`.
Using `__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `__searchFilter` is `%`.
The example below shows how to use `$__searchFilter` as part of the query field to enable searching for `hostname` while the user types in the dropdown select box.
> Important that you surround the `__searchFilter` expression with quotes as Grafana does not do this for you.
The example below shows how to use `__searchFilter` as part of the query field to enable searching for `hostname` while the user types in the dropdown select box.
Query
```sql
SELECT hostname FROM my_host WHERE hostname LIKE $__searchFilter
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
### Using Variables in Queries

View File

@ -282,17 +282,19 @@ the hosts variable only show hosts from the current selected region with a query
SELECT hostname FROM host WHERE region IN($region)
```
#### Using `$__searchFilter` to filter results in Query Variable
#### Using `__searchFilter` to filter results in Query Variable
> Available from Grafana 6.5 and above
Using `$__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `$__searchFilter` is `%`.
Using `__searchFilter` in the query field will filter the query result based on what the user types in the dropdown select box.
When nothing has been entered by the user the default value for `__searchFilter` is `%`.
The example below shows how to use `$__searchFilter` as part of the query field to enable searching for `hostname` while the user types in the dropdown select box.
> Important that you surround the `__searchFilter` expression with quotes as Grafana does not do this for you.
The example below shows how to use `__searchFilter` as part of the query field to enable searching for `hostname` while the user types in the dropdown select box.
Query
```sql
SELECT hostname FROM my_host WHERE hostname LIKE $__searchFilter
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
### Using Variables in Queries

View File

@ -2,9 +2,10 @@ import {
assignModelProperties,
containsSearchFilter,
containsVariable,
interpolateSearchFilter,
getSearchFilterScopedVar,
SEARCH_FILTER_VARIABLE,
} from '../variable';
import { ScopedVars } from '@grafana/data';
describe('containsVariable', () => {
describe('when checking if a string contains a variable', () => {
@ -92,84 +93,164 @@ describe('containsSearchFilter', () => {
});
});
describe(`when called with a query with ${SEARCH_FILTER_VARIABLE}`, () => {
it('then it should return false', () => {
const result = containsSearchFilter(`$app.${SEARCH_FILTER_VARIABLE}`);
describe(`when called with a query with $${SEARCH_FILTER_VARIABLE}`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.$${SEARCH_FILTER_VARIABLE}`);
expect(result).toBe(true);
});
});
describe(`when called with a query with [[${SEARCH_FILTER_VARIABLE}]]`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.[[${SEARCH_FILTER_VARIABLE}]]`);
expect(result).toBe(true);
});
});
describe(`when called with a query with \$\{${SEARCH_FILTER_VARIABLE}:regex\}`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.\$\{${SEARCH_FILTER_VARIABLE}:regex\}`);
expect(result).toBe(true);
});
});
});
describe('interpolateSearchFilter', () => {
describe('when called with a query without ${SEARCH_FILTER_VARIABLE}', () => {
it('then it should return query', () => {
const query = '$app.*';
const options = { searchFilter: 'filter' };
const wildcardChar = '*';
const quoteLiteral = false;
interface GetSearchFilterScopedVarScenario {
query: string;
wildcardChar: string;
options: { searchFilter?: string };
expected: ScopedVars;
}
const result = interpolateSearchFilter({
query,
options,
wildcardChar,
quoteLiteral,
const scenarios: GetSearchFilterScopedVarScenario[] = [
// testing the $__searchFilter notation
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the [[__searchFilter]] notation
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the ${__searchFilter:fmt} notation
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the no options
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: null,
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: null,
expected: { __searchFilter: { value: '*', text: '' } },
},
// testing the no search filter at all
{
query: 'abc.$def',
wildcardChar: '',
options: { searchFilter: '' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '*',
options: { searchFilter: '' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: {},
},
];
scenarios.map(scenario => {
describe('getSearchFilterScopedVar', () => {
describe(`when called with query:'${scenario.query}'`, () => {
describe(`and wildcardChar:'${scenario.wildcardChar}'`, () => {
describe(`and options:'${JSON.stringify(scenario.options, null, 0)}'`, () => {
it(`then the result should be ${JSON.stringify(scenario.expected, null, 0)}`, () => {
const { expected, ...args } = scenario;
expect(getSearchFilterScopedVar(args)).toEqual(expected);
});
expect(result).toEqual(query);
});
});
describe(`when called with a query with ${SEARCH_FILTER_VARIABLE}`, () => {
const query = `$app.${SEARCH_FILTER_VARIABLE}`;
describe('and no searchFilter is given', () => {
it(`then ${SEARCH_FILTER_VARIABLE} should be replaced by wildchar character`, () => {
const options = {};
const wildcardChar = '*';
const quoteLiteral = false;
const result = interpolateSearchFilter({
query,
options,
wildcardChar,
quoteLiteral,
});
expect(result).toEqual(`$app.*`);
});
});
describe('and searchFilter is given', () => {
const options = { searchFilter: 'filter' };
it(`then ${SEARCH_FILTER_VARIABLE} should be replaced with searchfilter and wildchar character`, () => {
const wildcardChar = '*';
const quoteLiteral = false;
const result = interpolateSearchFilter({
query,
options,
wildcardChar,
quoteLiteral,
});
expect(result).toEqual(`$app.filter*`);
});
describe(`and quoteLiteral is used`, () => {
it(`then the literal should be quoted`, () => {
const wildcardChar = '*';
const quoteLiteral = true;
const result = interpolateSearchFilter({
query,
options,
wildcardChar,
quoteLiteral,
});
expect(result).toEqual(`$app.'filter*'`);
});
});
});

View File

@ -1,7 +1,7 @@
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import { variableRegex } from 'app/features/templating/variable';
import { TimeRange, ScopedVars } from '@grafana/data';
import { ScopedVars, TimeRange } from '@grafana/data';
function luceneEscape(value: string) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
import { assignModelProperties } from 'app/core/utils/model_utils';
import { ScopedVars } from '@grafana/data';
/*
* This regex matches 3 types of variable reference with an optional format specifier
@ -15,31 +16,32 @@ export const variableRegexExec = (variableString: string) => {
return variableRegex.exec(variableString);
};
export const SEARCH_FILTER_VARIABLE = '$__searchFilter';
export const SEARCH_FILTER_VARIABLE = '__searchFilter';
export const containsSearchFilter = (query: string): boolean =>
query ? query.indexOf(SEARCH_FILTER_VARIABLE) !== -1 : false;
export interface InterpolateSearchFilterOptions {
export const getSearchFilterScopedVar = (args: {
query: string;
options: any;
wildcardChar: string;
quoteLiteral: boolean;
}
export const interpolateSearchFilter = (args: InterpolateSearchFilterOptions): string => {
const { query, wildcardChar, quoteLiteral } = args;
let { options } = args;
options: { searchFilter?: string };
}): ScopedVars => {
const { query, wildcardChar } = args;
if (!containsSearchFilter(query)) {
return query;
return {};
}
options = options || {};
let { options } = args;
const filter = options.searchFilter ? `${options.searchFilter}${wildcardChar}` : `${wildcardChar}`;
const replaceValue = quoteLiteral ? `'${filter}'` : filter;
options = options || { searchFilter: '' };
const value = options.searchFilter ? `${options.searchFilter}${wildcardChar}` : `${wildcardChar}`;
return query.replace(SEARCH_FILTER_VARIABLE, replaceValue);
return {
__searchFilter: {
value,
text: '',
},
};
};
export enum VariableRefresh {

View File

@ -7,7 +7,7 @@ import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
//Types
import { GraphiteQuery } from './types';
import { interpolateSearchFilter } from '../../../features/templating/variable';
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
export class GraphiteDatasource {
basicAuth: string;
@ -251,12 +251,10 @@ export class GraphiteDatasource {
metricFindQuery(query: string, optionalOptions: any) {
const options: any = optionalOptions || {};
const interpolatedQuery = interpolateSearchFilter({
query: this.templateSrv.replace(query),
options: optionalOptions,
wildcardChar: '*',
quoteLiteral: false,
});
let interpolatedQuery = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '', options: optionalOptions })
);
// special handling for tag_values(<tag>[,<expression>]*), this is used for template variables
let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/);
@ -289,6 +287,11 @@ export class GraphiteDatasource {
return this.getTagsAutoComplete(expressions, undefined, options);
}
interpolatedQuery = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '*', options: optionalOptions })
);
const httpOptions: any = {
method: 'POST',
url: '/metrics/find',

View File

@ -7,7 +7,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
//Types
import { MysqlQueryForInterpolation } from './types';
import { interpolateSearchFilter } from '../../../features/templating/variable';
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
export class MysqlDatasource {
id: any;
@ -131,12 +131,11 @@ export class MysqlDatasource {
refId = optionalOptions.variable.name;
}
const rawSql = interpolateSearchFilter({
query: this.templateSrv.replace(query, {}, this.interpolateVariable),
options: optionalOptions,
wildcardChar: '%',
quoteLiteral: true,
});
const rawSql = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '%', options: optionalOptions }),
this.interpolateVariable
);
const interpolatedQuery = {
refId: refId,

View File

@ -124,7 +124,7 @@ describe('MySQLDatasource', () => {
describe('When performing metricFindQuery with $__searchFilter and a searchFilter is given', () => {
let results: any;
let calledWith: any = {};
const query = 'select title from atable where title LIKE $__searchFilter';
const query = "select title from atable where title LIKE '$__searchFilter'";
const response = {
results: {
tempvar: {
@ -162,7 +162,7 @@ describe('MySQLDatasource', () => {
describe('When performing metricFindQuery with $__searchFilter but no searchFilter is given', () => {
let results: any;
let calledWith: any = {};
const query = 'select title from atable where title LIKE $__searchFilter';
const query = "select title from atable where title LIKE '$__searchFilter'";
const response = {
results: {
tempvar: {

View File

@ -7,7 +7,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
//Types
import { PostgresQueryForInterpolation } from './types';
import { interpolateSearchFilter } from '../../../features/templating/variable';
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
export class PostgresDatasource {
id: any;
@ -127,18 +127,17 @@ export class PostgresDatasource {
.then((data: any) => this.responseParser.transformAnnotationResponse(options, data));
}
metricFindQuery(query: string, optionalOptions: { variable?: any }) {
metricFindQuery(query: string, optionalOptions: { variable?: any; searchFilter?: string }) {
let refId = 'tempvar';
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
refId = optionalOptions.variable.name;
}
const rawSql = interpolateSearchFilter({
query: this.templateSrv.replace(query, {}, this.interpolateVariable),
options: optionalOptions,
wildcardChar: '%',
quoteLiteral: true,
});
const rawSql = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '%', options: optionalOptions }),
this.interpolateVariable
);
const interpolatedQuery = {
refId: refId,

View File

@ -131,7 +131,7 @@ describe('PostgreSQLDatasource', () => {
describe('When performing metricFindQuery with $__searchFilter and a searchFilter is given', () => {
let results: any;
let calledWith: any = {};
const query = 'select title from atable where title LIKE $__searchFilter';
const query = "select title from atable where title LIKE '$__searchFilter'";
const response = {
results: {
tempvar: {
@ -169,7 +169,7 @@ describe('PostgreSQLDatasource', () => {
describe('When performing metricFindQuery with $__searchFilter but no searchFilter is given', () => {
let results: any;
let calledWith: any = {};
const query = 'select title from atable where title LIKE $__searchFilter';
const query = "select title from atable where title LIKE '$__searchFilter'";
const response = {
results: {
tempvar: {

View File

@ -13,7 +13,7 @@ import { queryMetricTree } from './metricTree';
import { from, merge, Observable } from 'rxjs';
import { runStream } from './runStreams';
import templateSrv from 'app/features/templating/template_srv';
import { interpolateSearchFilter } from '../../../features/templating/variable';
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
type TestData = TimeSeries | TableData;
@ -126,12 +126,10 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
metricFindQuery(query: string, options: any) {
return new Promise<MetricFindValue[]>((resolve, reject) => {
setTimeout(() => {
const interpolatedQuery = interpolateSearchFilter({
query: templateSrv.replace(query),
options,
wildcardChar: '*',
quoteLiteral: false,
});
const interpolatedQuery = templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '*', options })
);
const children = queryMetricTree(interpolatedQuery);
const items = children.map(item => ({ value: item.name, text: item.name }));
resolve(items);