grafana/public/app/features/templating/template_srv.ts
Torkel Ödegaard 507c6e7d97
Templating: Replace __data , __field and __cell_n with macros (#65324)
* Templating: __data __field and __series macros

* filter out datacontext from json serialization

* Fix condition

* Update

* Added test cases for formatting data, and field macros
2023-04-05 11:10:33 +02:00

361 lines
10 KiB
TypeScript

import { escape, isString } from 'lodash';
import {
deprecationWarning,
ScopedVars,
TimeRange,
AdHocVariableFilter,
AdHocVariableModel,
TypedVariableModel,
ScopedVar,
} from '@grafana/data';
import {
getDataSourceSrv,
setTemplateSrv,
TemplateSrv as BaseTemplateSrv,
VariableInterpolation,
} from '@grafana/runtime';
import { sceneGraph, VariableCustomFormatterFn } from '@grafana/scenes';
import { VariableFormatID } from '@grafana/schema';
import { variableAdapters } from '../variables/adapters';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
import { isAdHoc } from '../variables/guard';
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
import { variableRegex } from '../variables/utils';
import { getFieldAccessor } from './fieldAccessorCache';
import { formatVariableValue } from './formatVariableValue';
import { macroRegistry } from './macroRegistry';
/**
* Internal regex replace function
*/
type ReplaceFunction = (fullMatch: string, variableName: string, fieldPath: string, format: string) => string;
export interface TemplateSrvDependencies {
getFilteredVariables: typeof getFilteredVariables;
getVariables: typeof getVariables;
getVariableWithName: typeof getVariableWithName;
}
const runtimeDependencies: TemplateSrvDependencies = {
getFilteredVariables,
getVariables,
getVariableWithName,
};
export class TemplateSrv implements BaseTemplateSrv {
private _variables: any[];
private regex = variableRegex;
private index: any = {};
private grafanaVariables = new Map<string, any>();
private timeRange?: TimeRange | null = null;
constructor(private dependencies: TemplateSrvDependencies = runtimeDependencies) {
this._variables = [];
}
init(variables: any, timeRange?: TimeRange) {
this._variables = variables;
this.timeRange = timeRange;
this.updateIndex();
}
/**
* @deprecated: this instance variable should not be used and will be removed in future releases
*
* Use getVariables function instead
*/
get variables(): any[] {
deprecationWarning('template_srv.ts', 'variables', 'getVariables');
return this.getVariables();
}
getVariables(): TypedVariableModel[] {
return this.dependencies.getVariables();
}
updateIndex() {
const existsOrEmpty = (value: any) => value || value === '';
this.index = this._variables.reduce((acc, currentValue) => {
if (currentValue.current && (currentValue.current.isNone || existsOrEmpty(currentValue.current.value))) {
acc[currentValue.name] = currentValue;
}
return acc;
}, {});
if (this.timeRange) {
const from = this.timeRange.from.valueOf().toString();
const to = this.timeRange.to.valueOf().toString();
this.index = {
...this.index,
['__from']: {
current: { value: from, text: from },
},
['__to']: {
current: { value: to, text: to },
},
};
}
}
updateTimeRange(timeRange: TimeRange) {
this.timeRange = timeRange;
this.updateIndex();
}
variableInitialized(variable: any) {
this.index[variable.name] = variable;
}
getAdhocFilters(datasourceName: string): AdHocVariableFilter[] {
let filters: any = [];
let ds = getDataSourceSrv().getInstanceSettings(datasourceName);
if (!ds) {
return [];
}
for (const variable of this.getAdHocVariables()) {
const variableUid = variable.datasource?.uid;
if (variableUid === ds.uid) {
filters = filters.concat(variable.filters);
} else if (variableUid?.indexOf('$') === 0) {
if (this.replace(variableUid) === datasourceName) {
filters = filters.concat(variable.filters);
}
}
}
return filters;
}
setGrafanaVariable(name: string, value: any) {
this.grafanaVariables.set(name, value);
}
/**
* @deprecated: setGlobalVariable function should not be used and will be removed in future releases
*
* Use addVariable action to add variables to Redux instead
*/
setGlobalVariable(name: string, variable: any) {
deprecationWarning('template_srv.ts', 'setGlobalVariable', '');
this.index = {
...this.index,
[name]: {
current: variable,
},
};
}
getVariableName(expression: string) {
this.regex.lastIndex = 0;
const match = this.regex.exec(expression);
if (!match) {
return null;
}
const variableName = match.slice(1).find((match) => match !== undefined);
return variableName;
}
containsTemplate(target: string | undefined): boolean {
if (!target) {
return false;
}
const name = this.getVariableName(target);
const variable = name && this.getVariableAtIndex(name);
return variable !== null && variable !== undefined;
}
variableExists(expression: string): boolean {
deprecationWarning('template_srv.ts', 'variableExists', 'containsTemplate');
return this.containsTemplate(expression);
}
highlightVariablesAsHtml(str: string) {
if (!str || !isString(str)) {
return str;
}
str = escape(str);
return this._replaceWithVariableRegex(str, undefined, (match, variableName) => {
if (this.getVariableAtIndex(variableName)) {
return '<span class="template-variable">' + match + '</span>';
}
return match;
});
}
getAllValue(variable: any) {
if (variable.allValue) {
return variable.allValue;
}
const values = [];
for (let i = 1; i < variable.options.length; i++) {
values.push(variable.options[i].value);
}
return values;
}
private getVariableValue(scopedVar: ScopedVar, fieldPath: string | undefined) {
if (fieldPath) {
return getFieldAccessor(fieldPath)(scopedVar.value);
}
return scopedVar.value;
}
private getVariableText(scopedVar: ScopedVar, value: any) {
if (scopedVar.value === value || typeof value !== 'string') {
return scopedVar.text;
}
return value;
}
replace(
target?: string,
scopedVars?: ScopedVars,
format?: string | Function | undefined,
interpolations?: VariableInterpolation[]
): string {
if (scopedVars && scopedVars.__sceneObject) {
return sceneGraph.interpolate(
scopedVars.__sceneObject.value,
target,
scopedVars,
format as string | VariableCustomFormatterFn | undefined
);
}
if (!target) {
return target ?? '';
}
this.regex.lastIndex = 0;
return this._replaceWithVariableRegex(target, format, (match, variableName, fieldPath, fmt) => {
const value = this._evaluateVariableExpression(match, variableName, fieldPath, fmt, 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 });
}
return value;
});
}
private _evaluateVariableExpression(
match: string,
variableName: string,
fieldPath: string,
format: string | VariableCustomFormatterFn | undefined,
scopedVars: ScopedVars | undefined
) {
const variable = this.getVariableAtIndex(variableName);
const scopedVar = scopedVars?.[variableName];
if (scopedVar) {
const value = this.getVariableValue(scopedVar, fieldPath);
const text = this.getVariableText(scopedVar, value);
if (value !== null && value !== undefined) {
return formatVariableValue(value, format, variable, text);
}
}
if (!variable) {
const macro = macroRegistry[variableName];
if (macro) {
return macro(match, fieldPath, scopedVars, format);
}
return match;
}
if (format === VariableFormatID.QueryParam || isAdHoc(variable)) {
const value = variableAdapters.get(variable.type).getValueForUrl(variable);
const text = isAdHoc(variable) ? variable.id : variable.current.text;
return formatVariableValue(value, format, variable, text);
}
const systemValue = this.grafanaVariables.get(variable.current.value);
if (systemValue) {
return formatVariableValue(systemValue, format, variable);
}
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 !== VariableFormatID.Text && format !== VariableFormatID.PercentEncode) {
return this.replace(value);
}
}
if (fieldPath) {
const fieldValue = this.getVariableValue({ value, text }, fieldPath);
if (fieldValue !== null && fieldValue !== undefined) {
return formatVariableValue(fieldValue, format, variable, text);
}
}
return formatVariableValue(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);
});
}
isAllValue(value: any) {
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
}
replaceWithText(target: string, scopedVars?: ScopedVars) {
deprecationWarning('template_srv.ts', 'replaceWithText()', 'replace(), and specify the :text format');
return this.replace(target, scopedVars, 'text');
}
private getVariableAtIndex(name: string) {
if (!name) {
return;
}
if (!this.index[name]) {
return this.dependencies.getVariableWithName(name);
}
return this.index[name];
}
private getAdHocVariables(): AdHocVariableModel[] {
return this.dependencies.getFilteredVariables(isAdHoc) as AdHocVariableModel[];
}
}
// Expose the template srv
const srv = new TemplateSrv();
setTemplateSrv(srv);
export const getTemplateSrv = () => srv;