1import { isArray, isEqual } from 'lodash';
2import { ScopedVars, UrlQueryMap, UrlQueryValue, VariableType } from '@grafana/data';
3import { getTemplateSrv } from '@grafana/runtime';
4
5import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from './state/types';
6import { QueryVariableModel, VariableModel, VariableRefresh } from './types';
7import { getTimeSrv } from '../dashboard/services/TimeSrv';
8import { variableAdapters } from './adapters';
9import { safeStringifyValue } from 'app/core/utils/explore';
10import { StoreState } from '../../types';
11import { getState } from '../../store/store';
12import { TransactionStatus } from './state/transactionReducer';
13
14/*
15 * This regex matches 3 types of variable reference with an optional format specifier
16 * \$(\w+)                          $var1
17 * \[\[([\s\S]+?)(?::(\w+))?\]\]    [[var2]] or [[var2:fmt2]]
18 * \${(\w+)(?::(\w+))?}             ${var3} or ${var3:fmt3}
19 */
20export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
21
22// Helper function since lastIndex is not reset
23export const variableRegexExec = (variableString: string) => {
24  variableRegex.lastIndex = 0;
25  return variableRegex.exec(variableString);
26};
27
28export const SEARCH_FILTER_VARIABLE = '__searchFilter';
29
30export const containsSearchFilter = (query: string | unknown): boolean =>
31  query && typeof query === 'string' ? query.indexOf(SEARCH_FILTER_VARIABLE) !== -1 : false;
32
33export const getSearchFilterScopedVar = (args: {
34  query: string;
35  wildcardChar: string;
36  options: { searchFilter?: string };
37}): ScopedVars => {
38  const { query, wildcardChar } = args;
39  if (!containsSearchFilter(query)) {
40    return {};
41  }
42
43  let { options } = args;
44
45  options = options || { searchFilter: '' };
46  const value = options.searchFilter ? `${options.searchFilter}${wildcardChar}` : `${wildcardChar}`;
47
48  return {
49    __searchFilter: {
50      value,
51      text: '',
52    },
53  };
54};
55
56export function containsVariable(...args: any[]) {
57  const variableName = args[args.length - 1];
58  args[0] = typeof args[0] === 'string' ? args[0] : safeStringifyValue(args[0]);
59  const variableString = args.slice(0, -1).join(' ');
60  const matches = variableString.match(variableRegex);
61  const isMatchingVariable =
62    matches !== null
63      ? matches.find((match) => {
64          const varMatch = variableRegexExec(match);
65          return varMatch !== null && varMatch.indexOf(variableName) > -1;
66        })
67      : false;
68
69  return !!isMatchingVariable;
70}
71
72export const isAllVariable = (variable: any): boolean => {
73  if (!variable) {
74    return false;
75  }
76
77  if (!variable.current) {
78    return false;
79  }
80
81  if (variable.current.value) {
82    const isArray = Array.isArray(variable.current.value);
83    if (isArray && variable.current.value.length && variable.current.value[0] === ALL_VARIABLE_VALUE) {
84      return true;
85    }
86
87    if (!isArray && variable.current.value === ALL_VARIABLE_VALUE) {
88      return true;
89    }
90  }
91
92  if (variable.current.text) {
93    const isArray = Array.isArray(variable.current.text);
94    if (isArray && variable.current.text.length && variable.current.text[0] === ALL_VARIABLE_TEXT) {
95      return true;
96    }
97
98    if (!isArray && variable.current.text === ALL_VARIABLE_TEXT) {
99      return true;
100    }
101  }
102
103  return false;
104};
105
106export const getCurrentText = (variable: any): string => {
107  if (!variable) {
108    return '';
109  }
110
111  if (!variable.current) {
112    return '';
113  }
114
115  if (!variable.current.text) {
116    return '';
117  }
118
119  if (Array.isArray(variable.current.text)) {
120    return variable.current.text.toString();
121  }
122
123  if (typeof variable.current.text !== 'string') {
124    return '';
125  }
126
127  return variable.current.text;
128};
129
130export function getTemplatedRegex(variable: QueryVariableModel, templateSrv = getTemplateSrv()): string {
131  if (!variable) {
132    return '';
133  }
134
135  if (!variable.regex) {
136    return '';
137  }
138
139  return templateSrv.replace(variable.regex, {}, 'regex');
140}
141
142export function getLegacyQueryOptions(variable: QueryVariableModel, searchFilter?: string, timeSrv = getTimeSrv()) {
143  const queryOptions: any = { range: undefined, variable, searchFilter };
144  if (variable.refresh === VariableRefresh.onTimeRangeChanged || variable.refresh === VariableRefresh.onDashboardLoad) {
145    queryOptions.range = timeSrv.timeRange();
146  }
147
148  return queryOptions;
149}
150
151export function getVariableRefresh(variable: VariableModel): VariableRefresh {
152  if (!variable || !variable.hasOwnProperty('refresh')) {
153    return VariableRefresh.never;
154  }
155
156  const queryVariable = variable as QueryVariableModel;
157
158  if (
159    queryVariable.refresh !== VariableRefresh.onTimeRangeChanged &&
160    queryVariable.refresh !== VariableRefresh.onDashboardLoad &&
161    queryVariable.refresh !== VariableRefresh.never
162  ) {
163    return VariableRefresh.never;
164  }
165
166  return queryVariable.refresh;
167}
168
169export function getVariableTypes(): Array<{ label: string; value: VariableType }> {
170  return variableAdapters
171    .list()
172    .filter((v) => v.id !== 'system')
173    .map(({ id, name }) => ({
174      label: name,
175      value: id,
176    }));
177}
178
179function getUrlValueForComparison(value: any): any {
180  if (isArray(value)) {
181    if (value.length === 0) {
182      value = undefined;
183    } else if (value.length === 1) {
184      value = value[0];
185    }
186  }
187
188  return value;
189}
190
191export interface UrlQueryType {
192  value: UrlQueryValue;
193  removed?: boolean;
194}
195
196export interface ExtendedUrlQueryMap extends Record<string, UrlQueryType> {}
197
198export function findTemplateVarChanges(query: UrlQueryMap, old: UrlQueryMap): ExtendedUrlQueryMap | undefined {
199  let count = 0;
200  const changes: ExtendedUrlQueryMap = {};
201
202  for (const key in query) {
203    if (!key.startsWith('var-')) {
204      continue;
205    }
206
207    let oldValue = getUrlValueForComparison(old[key]);
208    let newValue = getUrlValueForComparison(query[key]);
209
210    if (!isEqual(newValue, oldValue)) {
211      changes[key] = { value: query[key] };
212      count++;
213    }
214  }
215
216  for (const key in old) {
217    if (!key.startsWith('var-')) {
218      continue;
219    }
220
221    const value = old[key];
222
223    // ignore empty array values
224    if (isArray(value) && value.length === 0) {
225      continue;
226    }
227
228    if (!query.hasOwnProperty(key)) {
229      changes[key] = { value: '', removed: true }; // removed
230      count++;
231    }
232  }
233  return count ? changes : undefined;
234}
235
236export function ensureStringValues(value: any | any[]): string | string[] {
237  if (Array.isArray(value)) {
238    return value.map(String);
239  }
240
241  if (value === null || value === undefined) {
242    return '';
243  }
244
245  if (typeof value === 'number') {
246    return value.toString(10);
247  }
248
249  if (typeof value === 'string') {
250    return value;
251  }
252
253  if (typeof value === 'boolean') {
254    return value.toString();
255  }
256
257  return '';
258}
259
260export function hasOngoingTransaction(state: StoreState = getState()): boolean {
261  return state.templating.transaction.status !== TransactionStatus.NotStarted;
262}
263