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_2_name"], * rows: [ * [ , , ], * [ , , ] * ... * ] * } * * @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://www.rfc-editor.org/rfc/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; };