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