formatRegistry.ts 8.1 KB

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