gitlab-org--gitlab-foss/app/assets/javascripts/monitoring/csv_export.js

146 lines
3.8 KiB
JavaScript

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;
};