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