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      '&': '&amp;',
    18      '<': '&lt;',
    19      '>': '&gt;',
    20      '"': '&quot;',
    21      "'": '&#39;',
    22      '/': '&#x2F;',
    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    };