github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/utils/index.ts (about) 1 import moment from 'moment-timezone'; 2 3 import { PanelOptions, PanelType, PanelDefaultOptions } from '../pages/graph/Panel'; 4 import { PanelMeta } from '../pages/graph/PanelList'; 5 import { queryURL } from '../thanos/config'; 6 7 export const generateID = (): string => { 8 return `_${Math.random().toString(36).substr(2, 9)}`; 9 }; 10 11 export const byEmptyString = (p: string): boolean => p.length > 0; 12 13 export const isPresent = <T>(obj: T): obj is NonNullable<T> => obj !== null && obj !== undefined; 14 15 export const escapeHTML = (str: string): string => { 16 const entityMap: { [key: string]: string } = { 17 '&': '&', 18 '<': '<', 19 '>': '>', 20 '"': '"', 21 "'": ''', 22 '/': '/', 23 }; 24 25 return String(str).replace(/[&<>"'/]/g, function (s) { 26 return entityMap[s]; 27 }); 28 }; 29 30 export const metricToSeriesName = (labels: { [key: string]: string }): string => { 31 if (labels === null) { 32 return 'scalar'; 33 } 34 let tsName = (labels.__name__ || '') + '{'; 35 const labelStrings: string[] = []; 36 for (const label in labels) { 37 if (label !== '__name__') { 38 labelStrings.push(label + '="' + labels[label] + '"'); 39 } 40 } 41 tsName += labelStrings.join(', ') + '}'; 42 return tsName; 43 }; 44 45 export const parseDuration = (durationStr: string): number | null => { 46 if (durationStr === '') { 47 return null; 48 } 49 if (durationStr === '0') { 50 // Allow 0 without a unit. 51 return 0; 52 } 53 const durationRE = new RegExp('^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$'); 54 const matches = durationStr.match(durationRE); 55 if (!matches) { 56 return null; 57 } 58 59 let dur = 0; 60 61 // Parse the match at pos `pos` in the regex and use `mult` to turn that 62 // into ms, then add that value to the total parsed duration. 63 const m = (pos: number, mult: number) => { 64 if (matches[pos] === undefined) { 65 return; 66 } 67 const n = parseInt(matches[pos]); 68 dur += n * mult; 69 }; 70 71 m(2, 1000 * 60 * 60 * 24 * 365); // y 72 m(4, 1000 * 60 * 60 * 24 * 7); // w 73 m(6, 1000 * 60 * 60 * 24); // d 74 m(8, 1000 * 60 * 60); // h 75 m(10, 1000 * 60); // m 76 m(12, 1000); // s 77 m(14, 1); // ms 78 79 return dur; 80 }; 81 82 export const formatDuration = (d: number): string => { 83 let ms = d; 84 let r = ''; 85 if (ms === 0) { 86 return '0s'; 87 } 88 89 const f = (unit: string, mult: number, exact: boolean) => { 90 if (exact && ms % mult !== 0) { 91 return; 92 } 93 const v = Math.floor(ms / mult); 94 if (v > 0) { 95 r += `${v}${unit}`; 96 ms -= v * mult; 97 } 98 }; 99 100 // Only format years and weeks if the remainder is zero, as it is often 101 // easier to read 90d than 12w6d. 102 f('y', 1000 * 60 * 60 * 24 * 365, true); 103 f('w', 1000 * 60 * 60 * 24 * 7, true); 104 105 f('d', 1000 * 60 * 60 * 24, false); 106 f('h', 1000 * 60 * 60, false); 107 f('m', 1000 * 60, false); 108 f('s', 1000, false); 109 f('ms', 1, false); 110 111 return r; 112 }; 113 114 const MAX_TIME = BigInt('9223372036854775807'); 115 const MIN_TIME = 0; 116 117 export function parseTime(timeText: string): number { 118 return moment.utc(timeText).valueOf(); 119 } 120 121 export function formatTime(time: number): string { 122 return moment.utc(time).format('YYYY-MM-DD HH:mm:ss'); 123 } 124 125 export const isValidTime = (t: number): boolean => t > MIN_TIME && t < MAX_TIME; 126 127 export const now = (): number => moment().valueOf(); 128 129 export const humanizeDuration = (milliseconds: number): string => { 130 const sign = milliseconds < 0 ? '-' : ''; 131 const unsignedMillis = milliseconds < 0 ? -1 * milliseconds : milliseconds; 132 const duration = moment.duration(unsignedMillis, 'ms'); 133 const ms = Math.floor(duration.milliseconds()); 134 const s = Math.floor(duration.seconds()); 135 const m = Math.floor(duration.minutes()); 136 const h = Math.floor(duration.hours()); 137 const d = Math.floor(duration.asDays()); 138 if (d !== 0) { 139 return `${sign}${d}d ${h}h ${m}m ${s}s`; 140 } 141 if (h !== 0) { 142 return `${sign}${h}h ${m}m ${s}s`; 143 } 144 if (m !== 0) { 145 return `${sign}${m}m ${s}s`; 146 } 147 if (s !== 0) { 148 return `${sign}${s}.${ms}s`; 149 } 150 if (unsignedMillis > 0) { 151 return `${sign}${unsignedMillis.toFixed(3)}ms`; 152 } 153 return '0s'; 154 }; 155 156 export const formatRelative = (startStr: string, end: number): string => { 157 const start = parseTime(startStr); 158 if (start < 0) { 159 return 'Never'; 160 } 161 return humanizeDuration(end - start); 162 }; 163 164 const paramFormat = /^g\d+\..+=.+$/; 165 166 export const decodePanelOptionsFromQueryString = (query: string): PanelMeta[] => { 167 if (query === '') { 168 return []; 169 } 170 const urlParams = query.substring(1).split('&'); 171 172 return urlParams.reduce<PanelMeta[]>((panels, urlParam, i) => { 173 const panelsCount = panels.length; 174 const prefix = `g${panelsCount}.`; 175 if (urlParam.startsWith(`${prefix}expr=`)) { 176 const prefixLen = prefix.length; 177 return [ 178 ...panels, 179 { 180 id: generateID(), 181 key: `${panelsCount}`, 182 options: urlParams.slice(i).reduce((opts, param) => { 183 return param.startsWith(prefix) && paramFormat.test(param) 184 ? { ...opts, ...parseOption(param.substring(prefixLen)) } 185 : opts; 186 }, PanelDefaultOptions), 187 }, 188 ]; 189 } 190 return panels; 191 }, []); 192 }; 193 194 export const parseOption = (param: string): Partial<PanelOptions> => { 195 const [opt, val] = param.split('='); 196 const decodedValue = decodeURIComponent(val.replace(/\+/g, ' ')); 197 switch (opt) { 198 case 'expr': 199 return { expr: decodedValue }; 200 201 case 'tab': 202 return { type: decodedValue === '0' ? PanelType.Graph : PanelType.Table }; 203 204 case 'stacked': 205 return { stacked: decodedValue === '1' }; 206 207 case 'range_input': 208 const range = parseDuration(decodedValue); 209 return isPresent(range) ? { range } : {}; 210 211 case 'end_input': 212 case 'moment_input': 213 return { endTime: parseTime(decodedValue) }; 214 215 case 'step_input': 216 const resolution = parseInt(decodedValue); 217 return resolution > 0 ? { resolution } : {}; 218 219 case 'max_source_resolution': 220 return { maxSourceResolution: decodedValue }; 221 222 case 'deduplicate': 223 return { useDeduplication: decodedValue === '1' }; 224 225 case 'partial_response': 226 return { usePartialResponse: decodedValue === '1' }; 227 228 case 'store_matches': 229 return { storeMatches: JSON.parse(decodedValue) }; 230 231 case 'engine': 232 return { engine: decodedValue }; 233 234 case 'explain': 235 return { explain: decodedValue === '1' }; 236 } 237 return {}; 238 }; 239 240 export const formatParam = 241 (key: string) => 242 (paramName: string, value: number | string | boolean): string => { 243 return `g${key}.${paramName}=${encodeURIComponent(value)}`; 244 }; 245 246 export const toQueryString = ({ key, options }: PanelMeta): string => { 247 const formatWithKey = formatParam(key); 248 const { 249 expr, 250 type, 251 stacked, 252 range, 253 endTime, 254 resolution, 255 maxSourceResolution, 256 useDeduplication, 257 usePartialResponse, 258 storeMatches, 259 engine, 260 explain, 261 } = options; 262 const time = isPresent(endTime) ? formatTime(endTime) : false; 263 const urlParams = [ 264 formatWithKey('expr', expr), 265 formatWithKey('tab', type === PanelType.Graph ? 0 : 1), 266 formatWithKey('stacked', stacked ? 1 : 0), 267 formatWithKey('range_input', formatDuration(range)), 268 formatWithKey('max_source_resolution', maxSourceResolution), 269 formatWithKey('deduplicate', useDeduplication ? 1 : 0), 270 formatWithKey('partial_response', usePartialResponse ? 1 : 0), 271 formatWithKey('store_matches', JSON.stringify(storeMatches, ['name'])), 272 formatWithKey('engine', engine), 273 formatWithKey('explain', explain ? 1 : 0), 274 time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '', 275 isPresent(resolution) ? formatWithKey('step_input', resolution) : '', 276 ]; 277 return urlParams.filter(byEmptyString).join('&'); 278 }; 279 280 export const encodePanelOptionsToQueryString = (panels: PanelMeta[]): string => { 281 return `?${panels.map(toQueryString).join('&')}`; 282 }; 283 284 export const createExpressionLink = (expr: string): string => { 285 return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`; 286 }; 287 288 export const createExternalExpressionLink = (expr: string): string => { 289 const expLink = createExpressionLink(expr); 290 return `${queryURL}${expLink.replace(/^\.\./, '')}`; 291 }; 292 293 export const mapObjEntries = <T, key extends keyof T, Z>( 294 o: T, 295 cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z 296 ) => Object.entries(o).map(cb); 297 298 export const callAll = 299 (...fns: Array<(...args: any) => void>) => 300 (...args: any) => { 301 // eslint-disable-next-line prefer-spread 302 fns.filter(Boolean).forEach((fn) => fn.apply(null, args)); 303 };