1import kbn from 'app/core/utils/kbn';
2import { dateTime, Registry, RegistryItem, textUtil, VariableModel } from '@grafana/data';
3import { isArray, map, replace } from 'lodash';
4import { formatVariableLabel } from '../variables/shared/formatVariable';
5import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types';
6
7export interface FormatOptions {
8  value: any;
9  text: string;
10  args: string[];
11}
12
13export interface FormatRegistryItem extends RegistryItem {
14  formatter(options: FormatOptions, variable: VariableModel): string;
15}
16
17export enum FormatRegistryID {
18  lucene = 'lucene',
19  raw = 'raw',
20  regex = 'regex',
21  pipe = 'pipe',
22  distributed = 'distributed',
23  csv = 'csv',
24  html = 'html',
25  json = 'json',
26  percentEncode = 'percentencode',
27  singleQuote = 'singlequote',
28  doubleQuote = 'doublequote',
29  sqlString = 'sqlstring',
30  date = 'date',
31  glob = 'glob',
32  text = 'text',
33  queryParam = 'queryparam',
34}
35
36export const formatRegistry = new Registry<FormatRegistryItem>(() => {
37  const formats: FormatRegistryItem[] = [
38    {
39      id: FormatRegistryID.lucene,
40      name: 'Lucene',
41      description: 'Values are lucene escaped and multi-valued variables generate an OR expression',
42      formatter: ({ value }) => {
43        if (typeof value === 'string') {
44          return luceneEscape(value);
45        }
46
47        if (value instanceof Array && value.length === 0) {
48          return '__empty__';
49        }
50
51        const quotedValues = map(value, (val: string) => {
52          return '"' + luceneEscape(val) + '"';
53        });
54
55        return '(' + quotedValues.join(' OR ') + ')';
56      },
57    },
58    {
59      id: FormatRegistryID.raw,
60      name: 'raw',
61      description: 'Keep value as is',
62      formatter: ({ value }) => value,
63    },
64    {
65      id: FormatRegistryID.regex,
66      name: 'Regex',
67      description: 'Values are regex escaped and multi-valued variables generate a (<value>|<value>) expression',
68      formatter: ({ value }) => {
69        if (typeof value === 'string') {
70          return kbn.regexEscape(value);
71        }
72
73        const escapedValues = map(value, kbn.regexEscape);
74        if (escapedValues.length === 1) {
75          return escapedValues[0];
76        }
77        return '(' + escapedValues.join('|') + ')';
78      },
79    },
80    {
81      id: FormatRegistryID.pipe,
82      name: 'Pipe',
83      description: 'Values are separated by | character',
84      formatter: ({ value }) => {
85        if (typeof value === 'string') {
86          return value;
87        }
88        return value.join('|');
89      },
90    },
91    {
92      id: FormatRegistryID.distributed,
93      name: 'Distributed',
94      description: 'Multiple values are formatted like variable=value',
95      formatter: ({ value }, variable) => {
96        if (typeof value === 'string') {
97          return value;
98        }
99
100        value = map(value, (val: any, index: number) => {
101          if (index !== 0) {
102            return variable.name + '=' + val;
103          } else {
104            return val;
105          }
106        });
107        return value.join(',');
108      },
109    },
110    {
111      id: FormatRegistryID.csv,
112      name: 'Csv',
113      description: 'Comma-separated values',
114      formatter: ({ value }) => {
115        if (isArray(value)) {
116          return value.join(',');
117        }
118        return value;
119      },
120    },
121    {
122      id: FormatRegistryID.html,
123      name: 'HTML',
124      description: 'HTML escaping of values',
125      formatter: ({ value }) => {
126        if (isArray(value)) {
127          return textUtil.escapeHtml(value.join(', '));
128        }
129        return textUtil.escapeHtml(value);
130      },
131    },
132    {
133      id: FormatRegistryID.json,
134      name: 'JSON',
135      description: 'JSON stringify valu',
136      formatter: ({ value }) => {
137        return JSON.stringify(value);
138      },
139    },
140    {
141      id: FormatRegistryID.percentEncode,
142      name: 'Percent encode',
143      description: 'Useful for URL escaping values',
144      formatter: ({ value }) => {
145        // like glob, but url escaped
146        if (isArray(value)) {
147          return encodeURIComponentStrict('{' + value.join(',') + '}');
148        }
149        return encodeURIComponentStrict(value);
150      },
151    },
152    {
153      id: FormatRegistryID.singleQuote,
154      name: 'Single quote',
155      description: 'Single quoted values',
156      formatter: ({ value }) => {
157        // escape single quotes with backslash
158        const regExp = new RegExp(`'`, 'g');
159        if (isArray(value)) {
160          return map(value, (v: string) => `'${replace(v, regExp, `\\'`)}'`).join(',');
161        }
162        return `'${replace(value, regExp, `\\'`)}'`;
163      },
164    },
165    {
166      id: FormatRegistryID.doubleQuote,
167      name: 'Double quote',
168      description: 'Double quoted values',
169      formatter: ({ value }) => {
170        // escape double quotes with backslash
171        const regExp = new RegExp('"', 'g');
172        if (isArray(value)) {
173          return map(value, (v: string) => `"${replace(v, regExp, '\\"')}"`).join(',');
174        }
175        return `"${replace(value, regExp, '\\"')}"`;
176      },
177    },
178    {
179      id: FormatRegistryID.sqlString,
180      name: 'SQL string',
181      description: 'SQL string quoting and commas for use in IN statements and other scenarios',
182      formatter: ({ value }) => {
183        // escape single quotes by pairing them
184        const regExp = new RegExp(`'`, 'g');
185        if (isArray(value)) {
186          return map(value, (v) => `'${replace(v, regExp, "''")}'`).join(',');
187        }
188        return `'${replace(value, regExp, "''")}'`;
189      },
190    },
191    {
192      id: FormatRegistryID.date,
193      name: 'Date',
194      description: 'Format date in different ways',
195      formatter: ({ value, args }) => {
196        const arg = args[0] ?? 'iso';
197
198        switch (arg) {
199          case 'ms':
200            return value;
201          case 'seconds':
202            return `${Math.round(parseInt(value, 10)! / 1000)}`;
203          case 'iso':
204            return dateTime(parseInt(value, 10)).toISOString();
205          default:
206            return dateTime(parseInt(value, 10)).format(arg);
207        }
208      },
209    },
210    {
211      id: FormatRegistryID.glob,
212      name: 'Glob',
213      description: 'Format multi-valued variables using glob syntax, example {value1,value2}',
214      formatter: ({ value }) => {
215        if (isArray(value) && value.length > 1) {
216          return '{' + value.join(',') + '}';
217        }
218        return value;
219      },
220    },
221    {
222      id: FormatRegistryID.text,
223      name: 'Text',
224      description: 'Format variables in their text representation. Example in multi-variable scenario A + B + C.',
225      formatter: (options, variable) => {
226        if (typeof options.text === 'string') {
227          return options.value === ALL_VARIABLE_VALUE ? ALL_VARIABLE_TEXT : options.text;
228        }
229
230        const current = (variable as any)?.current;
231
232        if (!current) {
233          return options.value;
234        }
235
236        return formatVariableLabel(variable);
237      },
238    },
239    {
240      id: FormatRegistryID.queryParam,
241      name: 'Query parameter',
242      description:
243        'Format variables as URL parameters. Example in multi-variable scenario A + B + C => var-foo=A&var-foo=B&var-foo=C.',
244      formatter: (options, variable) => {
245        const { value } = options;
246        const { name } = variable;
247
248        if (Array.isArray(value)) {
249          return value.map((v) => formatQueryParameter(name, v)).join('&');
250        }
251
252        return formatQueryParameter(name, value);
253      },
254    },
255  ];
256
257  return formats;
258});
259
260function luceneEscape(value: string) {
261  return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
262}
263
264/**
265 * encode string according to RFC 3986; in contrast to encodeURIComponent()
266 * also the sub-delims "!", "'", "(", ")" and "*" are encoded;
267 * unicode handling uses UTF-8 as in ECMA-262.
268 */
269function encodeURIComponentStrict(str: string) {
270  return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
271    return '%' + c.charCodeAt(0).toString(16).toUpperCase();
272  });
273}
274
275function formatQueryParameter(name: string, value: string): string {
276  return `var-${name}=${encodeURIComponentStrict(value)}`;
277}
278
279export function isAllValue(value: any) {
280  return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
281}
282