All files / app/assets/javascripts/monitoring csv_export.js

100% Statements 38/38
62.5% Branches 5/8
100% Functions 15/15
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                      11x 90x                                                   11x 80x       85x 90x                                               11x 90x                                 11x 80x   80x 90x 333x 299x     333x       80x       299x   299x               90x   80x                       11x 80x 80x 80x   87x 80x 80x 80x   80x 2x     78x 299x   78x    
import { getSeriesLabel } from '~/helpers/monitor_helper';
 
/**
 * Returns a label for a header of the csv.
 *
 * Includes double quotes ("") in case the header includes commas or other separator.
 *
 * @param {String} axisLabel
 * @param {String} metricLabel
 * @param {Object} metricAttributes
 */
const csvHeader = (axisLabel, metricLabel, metricAttributes = {}) =>
  `${axisLabel} > ${getSeriesLabel(metricLabel, metricAttributes)}`;
 
/**
 * Returns an array with the header labels given a list of metrics
 *
 * ```
 * metrics = [
 *   {
 *      label: "..." // user-defined label
 *      result: [
 *        {
 *           metric: { ... } // metricAttributes
 *        },
 *        ...
 *      ]
 *   },
 *   ...
 * ]
 * ```
 *
 * When metrics have a `label` or `metricAttributes`, they are
 * used to generate the column name.
 *
 * @param {String} axisLabel - Main label
 * @param {Array} metrics - Metrics with results
 */
const csvMetricHeaders = (axisLabel, metrics) =>
  metrics.flatMap(({ label, result }) =>
    // The `metric` in a `result` is a map of `metricAttributes`
    // contains key-values to identify the series, rename it
    // here for clarity.
    result.map(({ metric: metricAttributes }) => {
      return csvHeader(axisLabel, label, metricAttributes);
    }),
  );
 
/**
 * Returns a (flat) array with all the values arrays in each
 * metric and series
 *
 * ```
 * metrics = [
 *   {
 *      result: [
 *        {
 *           values: [ ... ] // `values`
 *        },
 *        ...
 *      ]
 *   },
 *   ...
 * ]
 * ```
 *
 * @param {Array} metrics - Metrics with results
 */
const csvMetricValues = (metrics) =>
  metrics.flatMap(({ result }) => result.map((res) => res.values || []));
 
/**
 * Returns headers and rows for csv, sorted by their timestamp.
 *
 * {
 *   headers: ["timestamp", "<col_1_name>", "col_2_name"],
 *   rows: [
 *     [ <timestamp>, <col_1_value>, <col_2_value> ],
 *     [ <timestamp>, <col_1_value>, <col_2_value> ]
 *     ...
 *   ]
 * }
 *
 * @param {Array} metricHeaders
 * @param {Array} metricValues
 */
const csvData = (metricHeaders, metricValues) => {
  const rowsByTimestamp = {};
 
  metricValues.forEach((values, colIndex) => {
    values.forEach(([timestamp, value]) => {
      if (!rowsByTimestamp[timestamp]) {
        rowsByTimestamp[timestamp] = [];
      }
      // `value` should be in the right column
      rowsByTimestamp[timestamp][colIndex] = value;
    });
  });
 
  const rows = Object.keys(rowsByTimestamp)
    .sort()
    .map((timestamp) => {
      // force each row to have the same number of entries
      rowsByTimestamp[timestamp].length = metricHeaders.length;
      // add timestamp as the first entry
      return [timestamp, ...rowsByTimestamp[timestamp]];
    });
 
  // Escape double quotes and enclose headers:
  // "If double-quotes are used to enclose fields, then a double-quote
  // appearing inside a field must be escaped by preceding it with
  // another double quote."
  // https://tools.ietf.org/html/rfc4180#page-2
  const headers = metricHeaders.map((header) => `"${header.replace(/"/g, '""')}"`);
 
  return {
    headers: ['timestamp', ...headers],
    rows,
  };
};
 
/**
 * Returns dashboard panel's data in a string in CSV format
 *
 * @param {Object} graphData - Panel contents
 * @returns {String}
 */
export const graphDataToCsv = (graphData) => {
  const delimiter = ',';
  const br = '\r\n';
  const { metrics = [], y_label: axisLabel } = graphData;
 
  const metricsWithResults = metrics.filter((metric) => metric.result);
  const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults);
  const metricValues = csvMetricValues(metricsWithResults);
  const { headers, rows } = csvData(metricHeaders, metricValues);
 
  if (rows.length === 0) {
    return '';
  }
 
  const headerLine = headers.join(delimiter) + br;
  const lines = rows.map((row) => row.join(delimiter));
 
  return headerLine + lines.join(br) + br;
};