Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 79 additions & 13 deletions src/legacy/ui/public/agg_table/__tests__/_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,29 +262,95 @@ describe('AggTable Directive', function () {
});

describe('aggTable.toCsv()', function () {
it('escapes and formats the rows and columns properly', function () {
const $el = $compile('<kbn-agg-table table="table" dimensions="dimensions">')($scope);
it('escapes rows and columns properly', function () {
const $el = $compile('<kbn-agg-table table="table" dimensions="dimensions"></kbn-agg-table>')($scope);
$scope.$digest();

const $tableScope = $el.isolateScope();
const aggTable = $tableScope.aggTable;

$tableScope.table = {
columns: [
{ title: 'one' },
{ title: 'two' },
{ title: 'with double-quotes(")' }
{ id: 'a', name: 'one' },
{ id: 'b', name: 'two' },
{ id: 'c', name: 'with double-quotes(")' },
],
rows: [
[1, 2, '"foobar"']
]
{ a: 1, b: 2, c: '"foobar"' },
],
};

expect(aggTable.toCsv()).to.be(
'one,two,"with double-quotes("")"' + '\r\n' +
'1,2,"""foobar"""' + '\r\n'
);
});

it('exports rows and columns properly', async function () {
$scope.dimensions = {
buckets: [{ accessor: 0, params: {} }, { accessor: 2, params: {} }, { accessor: 4, params: {} }],
metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }]
};
$scope.table = (await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions)).tables[0];

const $el = $compile('<kbn-agg-table table="table" dimensions="dimensions"></kbn-agg-table>')($scope);
$scope.$digest();

const $tableScope = $el.isolateScope();
const aggTable = $tableScope.aggTable;
$tableScope.table = $scope.table;

const raw = aggTable.toCsv(false);
expect(raw).to.be(
'"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' +
'png,IT,win,412032,9299,0' + '\r\n' +
'png,IT,mac,412032,9299,9299' + '\r\n' +
'png,US,linux,412032,8293,3992' + '\r\n' +
'png,US,mac,412032,8293,3029' + '\r\n' +
'css,MX,win,412032,9299,4992' + '\r\n' +
'css,MX,mac,412032,9299,5892' + '\r\n' +
'css,US,linux,412032,8293,3992' + '\r\n' +
'css,US,mac,412032,8293,3029' + '\r\n' +
'html,CN,win,412032,9299,4992' + '\r\n' +
'html,CN,mac,412032,9299,5892' + '\r\n' +
'html,FR,win,412032,8293,3992' + '\r\n' +
'html,FR,mac,412032,8293,3029' + '\r\n'
);
});

it('exports formatted rows and columns properly', async function () {
$scope.dimensions = {
buckets: [{ accessor: 0, params: {} }, { accessor: 2, params: {} }, { accessor: 4, params: {} }],
metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }]
};
$scope.table = (await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions)).tables[0];

const $el = $compile('<kbn-agg-table table="table" dimensions="dimensions"></kbn-agg-table>')($scope);
$scope.$digest();

const $tableScope = $el.isolateScope();
const aggTable = $tableScope.aggTable;
$tableScope.table = $scope.table;

// Create our own converter since the ones we use for tests don't actually transform the provided value
$tableScope.formattedColumns[0].formatter.convert = v => `${v}_formatted`;

const formatted = aggTable.toCsv(true);
expect(formatted).to.be(
'"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' +
'"png_formatted",IT,win,412032,9299,0' + '\r\n' +
'"png_formatted",IT,mac,412032,9299,9299' + '\r\n' +
'"png_formatted",US,linux,412032,8293,3992' + '\r\n' +
'"png_formatted",US,mac,412032,8293,3029' + '\r\n' +
'"css_formatted",MX,win,412032,9299,4992' + '\r\n' +
'"css_formatted",MX,mac,412032,9299,5892' + '\r\n' +
'"css_formatted",US,linux,412032,8293,3992' + '\r\n' +
'"css_formatted",US,mac,412032,8293,3029' + '\r\n' +
'"html_formatted",CN,win,412032,9299,4992' + '\r\n' +
'"html_formatted",CN,mac,412032,9299,5892' + '\r\n' +
'"html_formatted",FR,win,412032,8293,3992' + '\r\n' +
'"html_formatted",FR,mac,412032,8293,3029' + '\r\n'
);
});
});

describe('aggTable.exportAsCsv()', function () {
Expand Down Expand Up @@ -313,13 +379,13 @@ describe('AggTable Directive', function () {
const saveAs = sinon.stub(aggTable, '_saveAs');
$tableScope.table = {
columns: [
{ title: 'one' },
{ title: 'two' },
{ title: 'with double-quotes(")' }
{ id: 'a', name: 'one' },
{ id: 'b', name: 'two' },
{ id: 'c', name: 'with double-quotes(")' },
],
rows: [
[1, 2, '"foobar"']
]
{ a: 1, b: 2, c: '"foobar"' },
],
};

aggTable.csv.filename = 'somefilename.csv';
Expand Down
6 changes: 4 additions & 2 deletions src/legacy/ui/public/agg_table/agg_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ uiModules

// escape each cell in each row
const csvRows = rows.map(function (row) {
return row.map(escape);
return Object.entries(row).map(([k, v]) => {
return escape(formatted ? columns.find(c => c.id === k).formatter.convert(v) : v);
});
});

// add the columns to the rows
csvRows.unshift(columns.map(function (col) {
return escape(col.title);
return escape(formatted ? col.title : col.name);
}));

return csvRows.map(function (row) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ import { fieldFormats } from '../../../registry/field_formats';

const config = chrome.getUiSettingsClient();

const defaultFormat = { convert: identity };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird thing I noticed when writing tests: the way we had defined the default formatter, the same object was being passed to every field that used the default format, which meant mutating convert on one field would actually change all the default fields.

I wasn't sure how likely it would be for this to cause problems in the real world, but went ahead and changed it anyway just to be safe.

const getConfig = (...args: any[]): any => config.get(...args);
const getDefaultFieldFormat = () => ({ convert: identity });

const getFieldFormat = (id: string, params: object) => {
const Format = fieldFormats.byId[id];
if (Format) {
return new Format(params, getConfig);
} else {
return defaultFormat;
return getDefaultFieldFormat();
}
};

export const getFormat = (mapping: any) => {
if (!mapping) {
return defaultFormat;
return getDefaultFieldFormat();
}
const { id } = mapping;
if (id === 'range') {
Expand Down