1import { escape, isString, property } from 'lodash';
2import { deprecationWarning, ScopedVars, TimeRange } from '@grafana/data';
3import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
4import { variableRegex } from '../variables/utils';
5import { isAdHoc } from '../variables/guard';
6import { AdHocVariableFilter, AdHocVariableModel, VariableModel } from '../variables/types';
7import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
8import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry';
9import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types';
10import { variableAdapters } from '../variables/adapters';
11
12interface FieldAccessorCache {
13  [key: string]: (obj: any) => any;
14}
15
16export interface TemplateSrvDependencies {
17  getFilteredVariables: typeof getFilteredVariables;
18  getVariables: typeof getVariables;
19  getVariableWithName: typeof getVariableWithName;
20}
21
22const runtimeDependencies: TemplateSrvDependencies = {
23  getFilteredVariables,
24  getVariables,
25  getVariableWithName,
26};
27
28export class TemplateSrv implements BaseTemplateSrv {
29  private _variables: any[];
30  private regex = variableRegex;
31  private index: any = {};
32  private grafanaVariables: any = {};
33  private timeRange?: TimeRange | null = null;
34  private fieldAccessorCache: FieldAccessorCache = {};
35
36  constructor(private dependencies: TemplateSrvDependencies = runtimeDependencies) {
37    this._variables = [];
38  }
39
40  init(variables: any, timeRange?: TimeRange) {
41    this._variables = variables;
42    this.timeRange = timeRange;
43    this.updateIndex();
44  }
45
46  /**
47   * @deprecated: this instance variable should not be used and will be removed in future releases
48   *
49   * Use getVariables function instead
50   */
51  get variables(): any[] {
52    deprecationWarning('template_srv.ts', 'variables', 'getVariables');
53    return this.getVariables();
54  }
55
56  getVariables(): VariableModel[] {
57    return this.dependencies.getVariables();
58  }
59
60  updateIndex() {
61    const existsOrEmpty = (value: any) => value || value === '';
62
63    this.index = this._variables.reduce((acc, currentValue) => {
64      if (currentValue.current && (currentValue.current.isNone || existsOrEmpty(currentValue.current.value))) {
65        acc[currentValue.name] = currentValue;
66      }
67      return acc;
68    }, {});
69
70    if (this.timeRange) {
71      const from = this.timeRange.from.valueOf().toString();
72      const to = this.timeRange.to.valueOf().toString();
73
74      this.index = {
75        ...this.index,
76        ['__from']: {
77          current: { value: from, text: from },
78        },
79        ['__to']: {
80          current: { value: to, text: to },
81        },
82      };
83    }
84  }
85
86  updateTimeRange(timeRange: TimeRange) {
87    this.timeRange = timeRange;
88    this.updateIndex();
89  }
90
91  variableInitialized(variable: any) {
92    this.index[variable.name] = variable;
93  }
94
95  getAdhocFilters(datasourceName: string): AdHocVariableFilter[] {
96    let filters: any = [];
97    let ds = getDataSourceSrv().getInstanceSettings(datasourceName);
98
99    if (!ds) {
100      return [];
101    }
102
103    for (const variable of this.getAdHocVariables()) {
104      const variableUid = variable.datasource?.uid;
105
106      if (variableUid === ds.uid || (variable.datasource == null && ds?.isDefault)) {
107        filters = filters.concat(variable.filters);
108      } else if (variableUid?.indexOf('$') === 0) {
109        if (this.replace(variableUid) === datasourceName) {
110          filters = filters.concat(variable.filters);
111        }
112      }
113    }
114
115    return filters;
116  }
117
118  formatValue(value: any, format: any, variable: any, text?: string): string {
119    // for some scopedVars there is no variable
120    variable = variable || {};
121
122    if (value === null || value === undefined) {
123      return '';
124    }
125
126    if (isAdHoc(variable) && format !== FormatRegistryID.queryParam) {
127      return '';
128    }
129
130    // if it's an object transform value to string
131    if (!Array.isArray(value) && typeof value === 'object') {
132      value = `${value}`;
133    }
134
135    if (typeof format === 'function') {
136      return format(value, variable, this.formatValue);
137    }
138
139    if (!format) {
140      format = FormatRegistryID.glob;
141    }
142
143    // some formats have arguments that come after ':' character
144    let args = format.split(':');
145    if (args.length > 1) {
146      format = args[0];
147      args = args.slice(1);
148    } else {
149      args = [];
150    }
151
152    let formatItem = formatRegistry.getIfExists(format);
153
154    if (!formatItem) {
155      console.error(`Variable format ${format} not found. Using glob format as fallback.`);
156      formatItem = formatRegistry.get(FormatRegistryID.glob);
157    }
158
159    const options: FormatOptions = { value, args, text: text ?? value };
160    return formatItem.formatter(options, variable);
161  }
162
163  setGrafanaVariable(name: string, value: any) {
164    this.grafanaVariables[name] = value;
165  }
166
167  /**
168   * @deprecated: setGlobalVariable function should not be used and will be removed in future releases
169   *
170   * Use addVariable action to add variables to Redux instead
171   */
172  setGlobalVariable(name: string, variable: any) {
173    deprecationWarning('template_srv.ts', 'setGlobalVariable', '');
174    this.index = {
175      ...this.index,
176      [name]: {
177        current: variable,
178      },
179    };
180  }
181
182  getVariableName(expression: string) {
183    this.regex.lastIndex = 0;
184    const match = this.regex.exec(expression);
185    if (!match) {
186      return null;
187    }
188    const variableName = match.slice(1).find((match) => match !== undefined);
189    return variableName;
190  }
191
192  variableExists(expression: string): boolean {
193    const name = this.getVariableName(expression);
194    const variable = name && this.getVariableAtIndex(name);
195    return variable !== null && variable !== undefined;
196  }
197
198  highlightVariablesAsHtml(str: string) {
199    if (!str || !isString(str)) {
200      return str;
201    }
202
203    str = escape(str);
204    this.regex.lastIndex = 0;
205    return str.replace(this.regex, (match, var1, var2, fmt2, var3) => {
206      if (this.getVariableAtIndex(var1 || var2 || var3)) {
207        return '<span class="template-variable">' + match + '</span>';
208      }
209      return match;
210    });
211  }
212
213  getAllValue(variable: any) {
214    if (variable.allValue) {
215      return variable.allValue;
216    }
217    const values = [];
218    for (let i = 1; i < variable.options.length; i++) {
219      values.push(variable.options[i].value);
220    }
221    return values;
222  }
223
224  private getFieldAccessor(fieldPath: string) {
225    const accessor = this.fieldAccessorCache[fieldPath];
226    if (accessor) {
227      return accessor;
228    }
229
230    return (this.fieldAccessorCache[fieldPath] = property(fieldPath));
231  }
232
233  private getVariableValue(variableName: string, fieldPath: string | undefined, scopedVars: ScopedVars) {
234    const scopedVar = scopedVars[variableName];
235    if (!scopedVar) {
236      return null;
237    }
238
239    if (fieldPath) {
240      return this.getFieldAccessor(fieldPath)(scopedVar.value);
241    }
242
243    return scopedVar.value;
244  }
245
246  private getVariableText(variableName: string, value: any, scopedVars: ScopedVars) {
247    const scopedVar = scopedVars[variableName];
248
249    if (!scopedVar) {
250      return null;
251    }
252
253    if (scopedVar.value === value || typeof value !== 'string') {
254      return scopedVar.text;
255    }
256
257    return value;
258  }
259
260  replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
261    if (!target) {
262      return target ?? '';
263    }
264
265    this.regex.lastIndex = 0;
266
267    return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
268      const variableName = var1 || var2 || var3;
269      const variable = this.getVariableAtIndex(variableName);
270      const fmt = fmt2 || fmt3 || format;
271
272      if (scopedVars) {
273        const value = this.getVariableValue(variableName, fieldPath, scopedVars);
274        const text = this.getVariableText(variableName, value, scopedVars);
275
276        if (value !== null && value !== undefined) {
277          return this.formatValue(value, fmt, variable, text);
278        }
279      }
280
281      if (!variable) {
282        return match;
283      }
284
285      if (fmt === FormatRegistryID.queryParam || isAdHoc(variable)) {
286        const value = variableAdapters.get(variable.type).getValueForUrl(variable);
287        const text = isAdHoc(variable) ? variable.id : variable.current.text;
288
289        return this.formatValue(value, fmt, variable, text);
290      }
291
292      const systemValue = this.grafanaVariables[variable.current.value];
293      if (systemValue) {
294        return this.formatValue(systemValue, fmt, variable);
295      }
296
297      let value = variable.current.value;
298      let text = variable.current.text;
299
300      if (this.isAllValue(value)) {
301        value = this.getAllValue(variable);
302        text = ALL_VARIABLE_TEXT;
303        // skip formatting of custom all values
304        if (variable.allValue && fmt !== FormatRegistryID.text) {
305          return this.replace(value);
306        }
307      }
308
309      if (fieldPath) {
310        const fieldValue = this.getVariableValue(variableName, fieldPath, {
311          [variableName]: { value, text },
312        });
313        if (fieldValue !== null && fieldValue !== undefined) {
314          return this.formatValue(fieldValue, fmt, variable, text);
315        }
316      }
317
318      const res = this.formatValue(value, fmt, variable, text);
319      return res;
320    });
321  }
322
323  isAllValue(value: any) {
324    return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
325  }
326
327  replaceWithText(target: string, scopedVars?: ScopedVars) {
328    deprecationWarning('template_srv.ts', 'replaceWithText()', 'replace(), and specify the :text format');
329    return this.replace(target, scopedVars, 'text');
330  }
331
332  private getVariableAtIndex(name: string) {
333    if (!name) {
334      return;
335    }
336
337    if (!this.index[name]) {
338      return this.dependencies.getVariableWithName(name);
339    }
340
341    return this.index[name];
342  }
343
344  private getAdHocVariables(): AdHocVariableModel[] {
345    return this.dependencies.getFilteredVariables(isAdHoc) as AdHocVariableModel[];
346  }
347}
348
349// Expose the template srv
350const srv = new TemplateSrv();
351
352setTemplateSrv(srv);
353
354export const getTemplateSrv = () => srv;
355