Templating: Optionally save interpolated expressions when replacing variables in a string (#65411)

* Testing a refactor

* update

* Update interface, test interoplations map

* Refactoring

* Add more explicit comment about new behavior

* Update packages/grafana-runtime/src/services/templateSrv.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
pull/65127/merge
Piotr Jamróz 2 years ago committed by GitHub
parent 0cff917f2a
commit b11186f946
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      packages/grafana-runtime/src/services/templateSrv.ts
  2. 66
      public/app/features/templating/template_srv.test.ts
  3. 135
      public/app/features/templating/template_srv.ts

@ -1,5 +1,23 @@
import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data';
/**
* Can be used to gain more information about an interpolation operation
*/
export interface VariableInterpolation {
/** The full matched expression including, example: ${varName.field:regex} */
match: string;
/** In the expression ${varName.field:regex} variableName is varName */
variableName: string;
/** In the expression ${varName.fields[0].name:regex} the fieldPath is fields[0].name */
fieldPath?: string;
/** In the expression ${varName:regex} the regex part is the format */
format?: string;
/** The formatted value of the variable expresion. Will equal match when variable not found or scopedVar was undefined or null **/
value: string;
// When value === match this will be true, meaning the variable was not found
found?: boolean;
}
/**
* Via the TemplateSrv consumers get access to all the available template variables
* that can be used within the current active dashboard.
@ -15,8 +33,19 @@ export interface TemplateSrv {
/**
* Replace the values within the target string. See also {@link InterpolateFunction}
*
* Note: interpolations array is being mutated by replace function by adding information about variables that
* have been interpolated during replacement. Variables that were specified in the target but not found in
* the list of available variables are also added to the array. See {@link VariableInterpolation} for more details.
*
* @param {VariableInterpolation[]} interpolations an optional map that is updated with interpolated variables
*/
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string;
replace(
target?: string,
scopedVars?: ScopedVars,
format?: string | Function,
interpolations?: VariableInterpolation[]
): string;
/**
* Checks if a target contains template variables.

@ -1,5 +1,5 @@
import { dateTime, TimeRange } from '@grafana/data';
import { setDataSourceSrv } from '@grafana/runtime';
import { setDataSourceSrv, VariableInterpolation } from '@grafana/runtime';
import { FormatRegistryID, TestVariable } from '@grafana/scenes';
import { VariableFormatID } from '@grafana/schema';
@ -128,6 +128,70 @@ describe('templateSrv', () => {
});
});
describe('replace with interpolations map', function () {
beforeEach(() => {
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'testValue' } }]);
});
it('replace can save interpolation result', () => {
let interpolations: VariableInterpolation[] = [];
const target = _templateSrv.replace(
'test.${test}.${scoped}.${nested.name}.${test}.${optionTest:raw}.$notfound',
{
scoped: { value: 'scopedValue', text: 'scopedText' },
optionTest: { value: 'optionTestValue', text: 'optionTestText' },
nested: { value: { name: 'nestedValue' } },
},
undefined,
interpolations
);
expect(target).toBe('test.testValue.scopedValue.nestedValue.testValue.optionTestValue.$notfound');
expect(interpolations.length).toBe(6);
expect(interpolations).toEqual([
{
match: '${test}',
found: true,
value: 'testValue',
variableName: 'test',
},
{
match: '${scoped}',
found: true,
value: 'scopedValue',
variableName: 'scoped',
},
{
fieldPath: 'name',
match: '${nested.name}',
found: true,
value: 'nestedValue',
variableName: 'nested',
},
{
match: '${test}',
found: true,
value: 'testValue',
variableName: 'test',
},
{
format: 'raw',
match: '${optionTest:raw}',
found: true,
value: 'optionTestValue',
variableName: 'optionTest',
},
{
match: '$notfound',
found: false,
value: '$notfound',
variableName: 'notfound',
},
]);
});
});
describe('getAdhocFilters', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv(key, [

@ -8,7 +8,12 @@ import {
AdHocVariableModel,
TypedVariableModel,
} from '@grafana/data';
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
import {
getDataSourceSrv,
setTemplateSrv,
TemplateSrv as BaseTemplateSrv,
VariableInterpolation,
} from '@grafana/runtime';
import { sceneGraph, FormatRegistryID, formatRegistry, VariableCustomFormatterFn } from '@grafana/scenes';
import { variableAdapters } from '../variables/adapters';
@ -23,6 +28,11 @@ interface FieldAccessorCache {
[key: string]: (obj: any) => any;
}
/**
* Internal regex replace function
*/
type ReplaceFunction = (fullMatch: string, variableName: string, fieldPath: string, format: string) => string;
export interface TemplateSrvDependencies {
getFilteredVariables: typeof getFilteredVariables;
getVariables: typeof getVariables;
@ -219,9 +229,8 @@ export class TemplateSrv implements BaseTemplateSrv {
}
str = escape(str);
this.regex.lastIndex = 0;
return str.replace(this.regex, (match, var1, var2, fmt2, var3) => {
if (this.getVariableAtIndex(var1 || var2 || var3)) {
return this._replaceWithVariableRegex(str, undefined, (match, variableName) => {
if (this.getVariableAtIndex(variableName)) {
return '<span class="template-variable">' + match + '</span>';
}
return match;
@ -275,7 +284,12 @@ export class TemplateSrv implements BaseTemplateSrv {
return value;
}
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
replace(
target?: string,
scopedVars?: ScopedVars,
format?: string | Function,
interpolations?: VariableInterpolation[]
): string {
if (scopedVars && scopedVars.__sceneObject) {
return sceneGraph.interpolate(
scopedVars.__sceneObject.value,
@ -291,63 +305,90 @@ export class TemplateSrv implements BaseTemplateSrv {
this.regex.lastIndex = 0;
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
const variableName = var1 || var2 || var3;
const variable = this.getVariableAtIndex(variableName);
let fmt = fmt2 || fmt3 || format;
return this._replaceWithVariableRegex(target, format, (match, variableName, fieldPath, fmt) => {
const value = this._evaluateVariableExpression(match, variableName, fieldPath, fmt, scopedVars);
if (scopedVars) {
const value = this.getVariableValue(variableName, fieldPath, scopedVars);
const text = this.getVariableText(variableName, value, scopedVars);
// If we get passed this interpolations map we will also record all the expressions that were replaced
if (interpolations) {
interpolations.push({ match, variableName, fieldPath, format: fmt, value, found: value !== match });
}
if (value !== null && value !== undefined) {
if (scopedVars[variableName].skipFormat) {
fmt = undefined;
}
return value;
});
}
return this.formatValue(value, fmt, variable, text);
private _evaluateVariableExpression(
match: string,
variableName: string,
fieldPath: string,
format: string | Function | undefined,
scopedVars: ScopedVars | undefined
) {
const variable = this.getVariableAtIndex(variableName);
if (scopedVars) {
const value = this.getVariableValue(variableName, fieldPath, scopedVars);
const text = this.getVariableText(variableName, value, scopedVars);
if (value !== null && value !== undefined) {
if (scopedVars[variableName]?.skipFormat) {
format = undefined;
}
}
if (!variable) {
return match;
return this.formatValue(value, format, variable, text);
}
}
if (fmt === FormatRegistryID.queryParam || isAdHoc(variable)) {
const value = variableAdapters.get(variable.type).getValueForUrl(variable);
const text = isAdHoc(variable) ? variable.id : variable.current.text;
if (!variable) {
return match;
}
return this.formatValue(value, fmt, variable, text);
}
if (format === FormatRegistryID.queryParam || isAdHoc(variable)) {
const value = variableAdapters.get(variable.type).getValueForUrl(variable);
const text = isAdHoc(variable) ? variable.id : variable.current.text;
const systemValue = this.grafanaVariables.get(variable.current.value);
if (systemValue) {
return this.formatValue(systemValue, fmt, variable);
}
return this.formatValue(value, format, variable, text);
}
let value = variable.current.value;
let text = variable.current.text;
const systemValue = this.grafanaVariables.get(variable.current.value);
if (systemValue) {
return this.formatValue(systemValue, format, variable);
}
if (this.isAllValue(value)) {
value = this.getAllValue(variable);
text = ALL_VARIABLE_TEXT;
// skip formatting of custom all values unless format set to text or percentencode
if (variable.allValue && fmt !== FormatRegistryID.text && fmt !== FormatRegistryID.percentEncode) {
return this.replace(value);
}
let value = variable.current.value;
let text = variable.current.text;
if (this.isAllValue(value)) {
value = this.getAllValue(variable);
text = ALL_VARIABLE_TEXT;
// skip formatting of custom all values unless format set to text or percentencode
if (variable.allValue && format !== FormatRegistryID.text && format !== FormatRegistryID.percentEncode) {
return this.replace(value);
}
}
if (fieldPath) {
const fieldValue = this.getVariableValue(variableName, fieldPath, {
[variableName]: { value, text },
});
if (fieldValue !== null && fieldValue !== undefined) {
return this.formatValue(fieldValue, fmt, variable, text);
}
if (fieldPath) {
const fieldValue = this.getVariableValue(variableName, fieldPath, {
[variableName]: { value, text },
});
if (fieldValue !== null && fieldValue !== undefined) {
return this.formatValue(fieldValue, format, variable, text);
}
}
const res = this.formatValue(value, fmt, variable, text);
return res;
return this.formatValue(value, format, variable, text);
}
/**
* Tries to unify the different variable format capture groups into a simpler replacer function
*/
private _replaceWithVariableRegex(text: string, format: string | Function | undefined, replace: ReplaceFunction) {
this.regex.lastIndex = 0;
return text.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
const variableName = var1 || var2 || var3;
const fmt = fmt2 || fmt3 || format;
return replace(match, variableName, fieldPath, fmt);
});
}

Loading…
Cancel
Save