grafana/grafana-polystat-panel

View on GitHub
src/data/composite_processor.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { includes as lodashIncludes } from 'lodash';
import { PolystatModel } from '../components/types';
import { getWorstSeries } from './threshold_processor';
import { ClickThroughTransformer } from './clickThroughTransformer';
import { stringToJsRegex, escapeStringForRegex, ScopedVars, InterpolateFunction, textUtil } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { CompositeItemType, CompositeMetric } from '../components/composites/types';
import { CUSTOM_SPLIT_DELIMITER } from './types';
import { ApplyGlobalRegexPattern } from './processor';
import { TimeFormatter } from './time_formatter';

export const resolveCompositeTemplates = (
  metricComposites: CompositeItemType[],
  replaceVariables: InterpolateFunction
): CompositeItemType[] => {
  const ret: CompositeItemType[] = [];
  metricComposites.forEach((item: CompositeItemType) => {
    const resolved = replaceVariables(item.name, undefined, customFormatter).split(CUSTOM_SPLIT_DELIMITER);
    // if the composite name has template syntax, mark it as isTemplated true
    const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::(\w+))?}/g;
    const matchResult = item.name.match(variableRegex);
    if (matchResult && matchResult.length > 0) {
      item.isTemplated = true;
    }
    resolved.forEach((newName: string) => {
      ret.push({
        ...item,
        name: newName,
        isTemplated: item.isTemplated,
      });
    });
  });

  return ret;
};

export const customFormatter = (value: any) => {
  if (Object.prototype.toString.call(value) === '[object Array]') {
    return value.join(CUSTOM_SPLIT_DELIMITER);
  }
  return value;
};

export const resolveMemberTemplates = (
  compositeName: string,
  members: CompositeMetric[],
  replaceVariables: InterpolateFunction
): CompositeMetric[] => {
  const ret: CompositeMetric[] = [];
  const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::(\w+))?}/g;
  members.forEach((member) => {
    // Resolve templates in series names
    if (member.seriesMatch) {
      const matchResult = member.seriesMatch.match(variableRegex);
      if (matchResult && matchResult.length > 0) {
        matchResult.forEach((aMatch) => {
          // expand the templatedName (append compositeName to the variables first)
          const scopedVars: ScopedVars = {
            compositeName: { text: 'compositeName', value: compositeName },
          };
          // template variables can be multi-select, or "all", iterate over each match
          const resolvedSeriesNames = replaceVariables(aMatch, scopedVars, customFormatter).split(CUSTOM_SPLIT_DELIMITER);
          // iterate over the array of names
          if (resolvedSeriesNames && resolvedSeriesNames.length) {
            resolvedSeriesNames.forEach((aName) => {
              if (aName.includes(compositeName)) {
                const newName = member.seriesMatch.replace(aMatch, aName);
                const escapedName = escapeStringForRegex(aName);
                const newNameEscaped = member.seriesMatch.replace(aMatch, escapedName);
                ret.push({
                  ...member,
                  seriesName: newName,
                  seriesNameEscaped: newNameEscaped,
                });
              }
            });
          }
        });
      } else {
        ret.push(member);
      }
    }
  });

  return ret;
};

const resolveMemberAliasTemplates = (name: string, matches: any): string => {
  const scopedVars: ScopedVars = {};
  matches.forEach((name: string, i: number) => {
    scopedVars[i] = { text: i, value: name };
  });
  if (matches.groups) {
    Object.keys(matches.groups).forEach((key) => {
      scopedVars[key.replace(/\s+/g, '_')] = { text: key, value: matches.groups[key] };
    });
  }
  return getTemplateSrv().replace(name, scopedVars);
};

/**
 * Copies values, leaves members empty
 */
const shallowClone = (item: PolystatModel): PolystatModel => {
  const clone: PolystatModel = {
    value: item.value,
    valueFormatted: item.valueFormatted,
    valueRounded: item.valueRounded,
    stats: item.stats,
    name: item.name,
    displayName: item.displayName,
    timestamp: item.timestamp,
    timestampFormatted: item.timestampFormatted,
    prefix: item.prefix,
    suffix: item.suffix,
    color: item.color,
    clickThrough: item.clickThrough,
    operatorName: item.operatorName,
    newTabEnabled: item.newTabEnabled,
    sanitizedURL: item.sanitizedURL,
    sanitizeURLEnabled: item.sanitizeURLEnabled,
    customClickthroughTargetEnabled: false,
    customClickthroughTarget: '',
    showName: item.showName,
    showValue: item.showValue,
    showTimestamp: item.showTimestamp,
    isComposite: item.isComposite,
    members: []
  };
  return clone;
};

export const ApplyComposites = (
  composites: CompositeItemType[],
  data: PolystatModel[],
  replaceVariables: InterpolateFunction,
  compositesGlobalAliasingEnabled: boolean,
  timeZone: string,
  globalRegexPattern?: string,
): PolystatModel[] => {
  if (!composites) {
    return data;
  }

  const filteredMetrics: number[] = [];
  const keepMetrics: number[] = [];
  const clonedComposites: PolystatModel[] = [];
  // the composite Name can be a template variable
  // the composite should only match specific metrics or expanded templated metrics that use the composite name
  const resolvedComposites = resolveCompositeTemplates(composites, replaceVariables);
  for (let i = 0; i < resolvedComposites.length; i++) {
    const matchedMetrics: number[] = [];
    const aComposite = resolvedComposites[i];
    if (!aComposite.showComposite) {
      continue;
    }
    let currentWorstSeries = null;
    // this should filter the members that are matches for the composite name
    const templatedMembers = resolveMemberTemplates(aComposite.name, aComposite.metrics, replaceVariables);
    for (let j = 0; j < templatedMembers.length; j++) {
      const aMetric = templatedMembers[j];
      // look for the matches to the pattern in the data
      for (let index = 0; index < data.length; index++) {
        // match regex
        // seriesName may not be defined yet, skip
        if (typeof aMetric.seriesMatch === 'undefined') {
          continue;
        }
        // name may not be escaped, check both
        let metricName = aMetric.seriesMatch;
        if (aMetric.seriesNameEscaped !== undefined) {
          metricName = aMetric.seriesNameEscaped;
        }
        const regex = stringToJsRegex(metricName);
        const matches = regex.exec(data[index].name);
        if (matches && matches.length > 0) {
          const seriesItem = data[index];
          // Template out the name of the metric using the alias
          if (aMetric.alias && aMetric.alias.length > 0) {
            seriesItem.displayName = resolveMemberAliasTemplates(aMetric.alias, matches);
          }

          // keep index of the matched metric
          matchedMetrics.push(index);
          // only hide if requested
          if (!aComposite.showMembers) {
            filteredMetrics.push(index);
          } else {
            keepMetrics.push(index);
          }
          if (aComposite.clickThrough && aComposite.clickThrough.length > 0) {
            let url = aComposite.clickThrough;
            // apply both types of transforms, one targeted at the data item index, and secondly the nth variant
            url = ClickThroughTransformer.transformComposite(aComposite.name, url);
            url = ClickThroughTransformer.transformSingleMetric(index, url, data);
            url = ClickThroughTransformer.transformNthMetric(url, data);
            // lastly apply template variables
            url = replaceVariables(url);
            seriesItem.clickThrough = url;
            seriesItem.sanitizedURL = textUtil.sanitizeUrl(url);
            seriesItem.customClickthroughTarget = aComposite.clickThroughCustomTarget;
            seriesItem.customClickthroughTargetEnabled = aComposite.clickThroughCustomTargetEnabled;
          }
          // process the timestamp display
          if (aComposite.showTimestampEnabled) {
            seriesItem.timestampFormatted = TimeFormatter(timeZone, data[index].timestamp, aComposite.showTimestampFormat);
            seriesItem.showTimestamp = true;
          }
        }
      }
    }
    if (matchedMetrics.length === 0) {
      continue;
    }
    // now determine the most triggered threshold

    for (let k = 0; k < matchedMetrics.length; k++) {
      const itemIndex = matchedMetrics[k];
      const seriesItem = data[itemIndex];
      // check thresholds
      if (currentWorstSeries === null) {
        currentWorstSeries = seriesItem;
      } else {
        currentWorstSeries = getWorstSeries(currentWorstSeries, seriesItem);
      }
    }
    // Prefix the valueFormatted with the actual metric name
    if (currentWorstSeries !== null) {
      const clone = shallowClone(currentWorstSeries);
      clone.name = aComposite.name;
      clone.displayName = aComposite.name;
      // tooltip/legend uses this to expand what values are inside the composite
      for (let index = 0; index < matchedMetrics.length; index++) {
        const itemIndex = matchedMetrics[index];
        clone.members.push({
          ...data[itemIndex],
          name: data[itemIndex].displayName || data[itemIndex].name,
        });
      }
      clone.thresholdLevel = currentWorstSeries.thresholdLevel;
      // currentWorstSeries.valueFormatted = currentWorstSeriesName + ': ' + currentWorstSeries.valueFormatted;
      // now push the composite into data
      // add the composite setting for showing the name/value to the new cloned model
      clone.showName = aComposite.showName;
      clone.showValue = aComposite.showValue;
      clone.showTimestamp = aComposite.showTimestampEnabled;
      clone.displayMode = aComposite.displayMode;
      clone.newTabEnabled = aComposite.clickThroughOpenNewTab;
      clone.sanitizeURLEnabled = aComposite.clickThroughSanitize;
      clone.customClickthroughTarget = aComposite.clickThroughCustomTarget;
      clone.customClickthroughTargetEnabled = aComposite.clickThroughCustomTargetEnabled;
      // mark this series as a composite
      clone.isComposite = true;
      clonedComposites.push(clone);
    }
  }
  // now merge the clonedComposites into data
  if(compositesGlobalAliasingEnabled && globalRegexPattern) {
    Array.prototype.push.apply(data, ApplyGlobalRegexPattern(clonedComposites, globalRegexPattern))
  } else {
    Array.prototype.push.apply(data, clonedComposites);
  }
  // remove the keepMetrics from the filteredMetrics list
  // these have been marked by at least one composite to be displayed
  for (let i = 0; i < keepMetrics.length; i++) {
    const keptMetric = keepMetrics[i];
    const location = filteredMetrics.indexOf(keptMetric);
    if (location >= 0) {
      filteredMetrics.splice(location, 1);
    }
  }
  // sort by value descending
  filteredMetrics.sort((a, b) => {
    return b - a;
  });
  // now remove the filtered metrics from final list
  // remove filtered metrics, use splice in reverse order
  for (let i = data.length; i >= 0; i--) {
    if (lodashIncludes(filteredMetrics, i)) {
      data.splice(i, 1);
    }
  }
  return data;
};