From 7aeed4d987f604d5a583ce30862ed192423a04b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Nov 2019 06:43:03 +0100 Subject: [PATCH] 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 --- docs/sources/features/datasources/graphite.md | 14 +- docs/sources/features/datasources/mysql.md | 12 +- docs/sources/features/datasources/postgres.md | 12 +- .../templating/specs/variable.test.ts | 219 ++++++++++++------ .../app/features/templating/template_srv.ts | 2 +- public/app/features/templating/variable.ts | 32 +-- .../plugins/datasource/graphite/datasource.ts | 17 +- .../plugins/datasource/mysql/datasource.ts | 13 +- .../datasource/mysql/specs/datasource.test.ts | 4 +- .../plugins/datasource/postgres/datasource.ts | 15 +- .../postgres/specs/datasource.test.ts | 4 +- .../plugins/datasource/testdata/datasource.ts | 12 +- 12 files changed, 224 insertions(+), 132 deletions(-) diff --git a/docs/sources/features/datasources/graphite.md b/docs/sources/features/datasources/graphite.md index fe9f2a73f7b..29eb1f3a41f 100644 --- a/docs/sources/features/datasources/graphite.md +++ b/docs/sources/features/datasources/graphite.md @@ -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. diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index fd73693f483..eb09eafbc32 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -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 diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 59a5c880c54..a88e70b355b 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -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 diff --git a/public/app/features/templating/specs/variable.test.ts b/public/app/features/templating/specs/variable.test.ts index 3023006d54d..1590bb939aa 100644 --- a/public/app/features/templating/specs/variable.test.ts +++ b/public/app/features/templating/specs/variable.test.ts @@ -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: {}, + }, +]; - expect(result).toEqual(query); - }); - }); +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; - 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(getSearchFilterScopedVar(args)).toEqual(expected); }); - - expect(result).toEqual(`$app.'filter*'`); }); }); }); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 42358aa5f0d..d35e7241649 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -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'); diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 60b79cea9b1..3687ec5742a 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -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 { diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 87aac2048ce..5e74778a912 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -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([,]*), 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', diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 63230de37c8..7d28dd738fb 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -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, diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 295b96b077d..7f5e9e39942 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -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: { diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 13b80558bb6..7fc4a5acce9 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -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, diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts index 4625e0be12a..7a35a4cf530 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.test.ts @@ -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: { diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index a465fc318c8..3dae81781a5 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -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 { metricFindQuery(query: string, options: any) { return new Promise((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);