diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.jest.ts new file mode 100644 index 00000000000..bbb894094ff --- /dev/null +++ b/public/app/core/specs/file_export.jest.ts @@ -0,0 +1,64 @@ +import * as fileExport from '../utils/file_export'; +import { beforeEach, expect } from 'test/lib/common'; + +describe('file_export', () => { + let ctx: any = {}; + + beforeEach(() => { + ctx.seriesList = [ + { + alias: 'series_1', + datapoints: [ + [1, 1500026100000], + [2, 1500026200000], + [null, 1500026300000], + [null, 1500026400000], + [null, 1500026500000], + [6, 1500026600000], + ], + }, + { + alias: 'series_2', + datapoints: [[11, 1500026100000], [12, 1500026200000], [13, 1500026300000], [15, 1500026500000]], + }, + ]; + + ctx.timeFormat = 'X'; // Unix timestamp (seconds) + }); + + describe('when exporting series as rows', () => { + it('should export points in proper order', () => { + let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); + const expectedText = + 'Series;Time;Value\n' + + 'series_1;1500026100;1\n' + + 'series_1;1500026200;2\n' + + 'series_1;1500026300;null\n' + + 'series_1;1500026400;null\n' + + 'series_1;1500026500;null\n' + + 'series_1;1500026600;6\n' + + 'series_2;1500026100;11\n' + + 'series_2;1500026200;12\n' + + 'series_2;1500026300;13\n' + + 'series_2;1500026500;15\n'; + + expect(text).toBe(expectedText); + }); + }); + + describe('when exporting series as columns', () => { + it('should export points in proper order', () => { + let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); + const expectedText = + 'Time;series_1;series_2\n' + + '1500026100;1;11\n' + + '1500026200;2;12\n' + + '1500026300;null;13\n' + + '1500026400;null;null\n' + + '1500026500;null;15\n' + + '1500026600;6;null\n'; + + expect(text).toBe(expectedText); + }); + }); +}); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 1ebd5b90a86..670326fc068 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -3,19 +3,27 @@ import moment from 'moment'; import { saveAs } from 'file-saver'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; +const POINT_TIME_INDEX = 1; +const POINT_VALUE_INDEX = 0; -export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { +export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n'; _.each(seriesList, function(series) { _.each(series.datapoints, function(dp) { - text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n'; + text += + series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n'; }); }); + return text; +} + +export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { + var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); saveSaveBlob(text, 'grafana_data_export.csv'); } -export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = (excel ? 'sep=;\n' : '') + 'Time;'; +export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { + let text = (excel ? 'sep=;\n' : '') + 'Time;'; // add header _.each(seriesList, function(series) { text += series.alias + ';'; @@ -24,14 +32,15 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL text += '\n'; // process data + seriesList = mergeSeriesByTime(seriesList); var dataArr = [[]]; var sIndex = 1; _.each(seriesList, function(series) { var cIndex = 0; dataArr.push([]); _.each(series.datapoints, function(dp) { - dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat); - dataArr[sIndex][cIndex] = dp[0]; + dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat); + dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX]; cIndex++; }); sIndex++; @@ -46,6 +55,44 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL text = text.substring(0, text.length - 1); text += '\n'; } + + return text; +} + +/** + * Collect all unique timestamps from series list and use it to fill + * missing points by null. + */ +function mergeSeriesByTime(seriesList) { + let timestamps = []; + for (let i = 0; i < seriesList.length; i++) { + let seriesPoints = seriesList[i].datapoints; + for (let j = 0; j < seriesPoints.length; j++) { + timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); + } + } + timestamps = _.sortedUniq(timestamps.sort()); + + for (let i = 0; i < seriesList.length; i++) { + let seriesPoints = seriesList[i].datapoints; + let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]); + let extendedSeries = []; + let pointIndex; + for (let j = 0; j < timestamps.length; j++) { + pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]); + if (pointIndex !== -1) { + extendedSeries.push(seriesPoints[pointIndex]); + } else { + extendedSeries.push([null, timestamps[j]]); + } + } + seriesList[i].datapoints = extendedSeries; + } + return seriesList; +} + +export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { + let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); saveSaveBlob(text, 'grafana_data_export.csv'); }