Explore: reuse table merge from table panel
- Extracted table panel's merge logic to combine multiple tables into one - Put the merge logic into the table model as it merges multiple table models - make use of merge in Explore's table query response handler - copied tests over to table model spec, kept essential tests in transformer spec
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import TableModel from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
|
|
||||||
describe('when sorting table desc', () => {
|
describe('when sorting table desc', () => {
|
||||||
let table;
|
let table;
|
||||||
@@ -79,3 +79,118 @@ describe('when sorting with nulls', () => {
|
|||||||
expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']);
|
expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('mergeTables', () => {
|
||||||
|
const time = new Date().getTime();
|
||||||
|
|
||||||
|
const singleTable = new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }],
|
||||||
|
rows: [[time, 'Label Value 1', 42]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const multipleTablesSameColumns = [
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #A' }],
|
||||||
|
rows: [[time, 'Label Value 1', 'Label Value 2', 42]],
|
||||||
|
}),
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #B' }],
|
||||||
|
rows: [[time, 'Label Value 1', 'Label Value 2', 13]],
|
||||||
|
}),
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }],
|
||||||
|
rows: [[time, 'Label Value 1', 'Label Value 2', 4]],
|
||||||
|
}),
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }],
|
||||||
|
rows: [[time, 'Label Value 1', 'Label Value 2', 7]],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const multipleTablesDifferentColumns = [
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }],
|
||||||
|
rows: [[time, 'Label Value 1', 42]],
|
||||||
|
}),
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 2' }, { text: 'Value #B' }],
|
||||||
|
rows: [[time, 'Label Value 2', 13]],
|
||||||
|
}),
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #C' }],
|
||||||
|
rows: [[time, 'Label Value 3', 7]],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return the single table as is', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
||||||
|
expect(table.columns.length).toBe(3);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(table.columns[2].text).toBe('Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the union of columns for multiple tables', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns);
|
||||||
|
expect(table.columns.length).toBe(6);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(table.columns[2].text).toBe('Label Key 2');
|
||||||
|
expect(table.columns[3].text).toBe('Value #A');
|
||||||
|
expect(table.columns[4].text).toBe('Value #B');
|
||||||
|
expect(table.columns[5].text).toBe('Value #C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 1 row for a single table', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
||||||
|
expect(table.rows.length).toBe(1);
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 rows for a multiple tables with same column values plus one extra row', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns);
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[0][3]).toBe(42);
|
||||||
|
expect(table.rows[0][4]).toBe(13);
|
||||||
|
expect(table.rows[0][5]).toBe(4);
|
||||||
|
expect(table.rows[1][0]).toBe(time);
|
||||||
|
expect(table.rows[1][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[1][2]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[1][3]).toBeUndefined();
|
||||||
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
|
expect(table.rows[1][5]).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 rows for multiple tables with different column values', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesDifferentColumns);
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.columns.length).toBe(6);
|
||||||
|
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe(42);
|
||||||
|
expect(table.rows[0][3]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[0][4]).toBe(13);
|
||||||
|
expect(table.rows[0][5]).toBeUndefined();
|
||||||
|
|
||||||
|
expect(table.rows[1][0]).toBe(time);
|
||||||
|
expect(table.rows[1][1]).toBe('Label Value 3');
|
||||||
|
expect(table.rows[1][2]).toBeUndefined();
|
||||||
|
expect(table.rows[1][3]).toBeUndefined();
|
||||||
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
|
expect(table.rows[1][5]).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
text: string;
|
text: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -14,11 +16,20 @@ export default class TableModel {
|
|||||||
type: string;
|
type: string;
|
||||||
columnMap: any;
|
columnMap: any;
|
||||||
|
|
||||||
constructor() {
|
constructor(table?: any) {
|
||||||
this.columns = [];
|
this.columns = [];
|
||||||
this.columnMap = {};
|
this.columnMap = {};
|
||||||
this.rows = [];
|
this.rows = [];
|
||||||
this.type = 'table';
|
this.type = 'table';
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
if (table.columns) {
|
||||||
|
table.columns.forEach(col => this.addColumn(col));
|
||||||
|
}
|
||||||
|
if (table.rows) {
|
||||||
|
table.rows.forEach(row => this.addRow(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(options) {
|
sort(options) {
|
||||||
@@ -52,3 +63,100 @@ export default class TableModel {
|
|||||||
this.rows.push(row);
|
this.rows.push(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if both rows have matching non-empty fields as well as matching
|
||||||
|
// indexes where one field is empty and the other is not
|
||||||
|
function areRowsMatching(columns, row, otherRow) {
|
||||||
|
let foundFieldToMatch = false;
|
||||||
|
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
||||||
|
if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
|
||||||
|
if (row[columnIndex] !== otherRow[columnIndex]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
|
||||||
|
foundFieldToMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundFieldToMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
|
||||||
|
const model = dst || new TableModel();
|
||||||
|
|
||||||
|
// Single query returns data columns and rows as is
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
model.columns = [...tables[0].columns];
|
||||||
|
model.rows = [...tables[0].rows];
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track column indexes of union: name -> index
|
||||||
|
const columnNames = {};
|
||||||
|
|
||||||
|
// Union of all non-value columns
|
||||||
|
const columnsUnion = tables.slice().reduce((acc, series) => {
|
||||||
|
series.columns.forEach(col => {
|
||||||
|
const { text } = col;
|
||||||
|
if (columnNames[text] === undefined) {
|
||||||
|
columnNames[text] = acc.length;
|
||||||
|
acc.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Map old column index to union index per series, e.g.,
|
||||||
|
// given columnNames {A: 0, B: 1} and
|
||||||
|
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
||||||
|
const columnIndexMapper = tables.map(series => series.columns.map(col => columnNames[col.text]));
|
||||||
|
|
||||||
|
// Flatten rows of all series and adjust new column indexes
|
||||||
|
const flattenedRows = tables.reduce((acc, series, seriesIndex) => {
|
||||||
|
const mapper = columnIndexMapper[seriesIndex];
|
||||||
|
series.rows.forEach(row => {
|
||||||
|
const alteredRow = [];
|
||||||
|
// Shifting entries according to index mapper
|
||||||
|
mapper.forEach((to, from) => {
|
||||||
|
alteredRow[to] = row[from];
|
||||||
|
});
|
||||||
|
acc.push(alteredRow);
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Merge rows that have same values for columns
|
||||||
|
const mergedRows = {};
|
||||||
|
const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
|
||||||
|
if (!mergedRows[rowIndex]) {
|
||||||
|
// Look from current row onwards
|
||||||
|
let offset = rowIndex + 1;
|
||||||
|
// More than one row can be merged into current row
|
||||||
|
while (offset < flattenedRows.length) {
|
||||||
|
// Find next row that could be merged
|
||||||
|
const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset);
|
||||||
|
if (match > -1) {
|
||||||
|
const matchedRow = flattenedRows[match];
|
||||||
|
// Merge values from match into current row if there is a gap in the current row
|
||||||
|
for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
|
||||||
|
if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
|
||||||
|
row[columnIndex] = matchedRow[columnIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't visit this row again
|
||||||
|
mergedRows[match] = matchedRow;
|
||||||
|
// Keep looking for more rows to merge
|
||||||
|
offset = match + 1;
|
||||||
|
} else {
|
||||||
|
// No match found, stop looking
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc.push(row);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
model.columns = columnsUnion;
|
||||||
|
model.rows = compactedRows;
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ResetStyles from 'app/core/components/Picker/ResetStyles';
|
|||||||
import PickerOption from 'app/core/components/Picker/PickerOption';
|
import PickerOption from 'app/core/components/Picker/PickerOption';
|
||||||
import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
|
import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
|
||||||
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
||||||
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
|
|
||||||
import ElapsedTime from './ElapsedTime';
|
import ElapsedTime from './ElapsedTime';
|
||||||
import QueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
@@ -389,8 +390,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
to: parseDate(range.to, true),
|
to: parseDate(range.to, true),
|
||||||
};
|
};
|
||||||
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||||
const targets = this.queryExpressions.map(q => ({
|
const targets = this.queryExpressions.map((q, i) => ({
|
||||||
...targetOptions,
|
...targetOptions,
|
||||||
|
// Target identifier is needed for table transformations
|
||||||
|
refId: i + 1,
|
||||||
expr: q,
|
expr: q,
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
@@ -437,7 +440,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const res = await datasource.query(options);
|
const res = await datasource.query(options);
|
||||||
const tableModel = res.data[0];
|
const tableModel = mergeTablesIntoModel(new TableModel(), ...res.data);
|
||||||
const latency = Date.now() - now;
|
const latency = Date.now() - now;
|
||||||
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
|
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
|
||||||
this.onQuerySuccess(datasource.meta.id, queries);
|
this.onQuerySuccess(datasource.meta.id, queries);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import ReactTable from 'react-table';
|
|||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
const EMPTY_TABLE = new TableModel();
|
const EMPTY_TABLE = new TableModel();
|
||||||
|
// Identify columns that contain values
|
||||||
|
const VALUE_REGEX = /^[Vv]alue #\d+/;
|
||||||
|
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
data: TableModel;
|
data: TableModel;
|
||||||
@@ -34,6 +36,7 @@ export default class Table extends PureComponent<TableProps> {
|
|||||||
const columns = tableModel.columns.map(({ filterable, text }) => ({
|
const columns = tableModel.columns.map(({ filterable, text }) => ({
|
||||||
Header: text,
|
Header: text,
|
||||||
accessor: text,
|
accessor: text,
|
||||||
|
className: VALUE_REGEX.test(text) ? 'text-right' : '',
|
||||||
show: text !== 'Time',
|
show: text !== 'Time',
|
||||||
Cell: row => <span className={filterable ? 'link' : ''}>{row.value}</span>,
|
Cell: row => <span className={filterable ? 'link' : ''}>{row.value}</span>,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -143,24 +143,6 @@ describe('when transforming time series table', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const multipleQueriesDataDifferentLabels = [
|
|
||||||
{
|
|
||||||
type: 'table',
|
|
||||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }],
|
|
||||||
rows: [[time, 'Label Value 1', 42]],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'table',
|
|
||||||
columns: [{ text: 'Time' }, { text: 'Label Key 2' }, { text: 'Value #B' }],
|
|
||||||
rows: [[time, 'Label Value 2', 13]],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'table',
|
|
||||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #C' }],
|
|
||||||
rows: [[time, 'Label Value 3', 7]],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('getColumns', () => {
|
describe('getColumns', () => {
|
||||||
it('should return data columns given a single query', () => {
|
it('should return data columns given a single query', () => {
|
||||||
const columns = transformers[transform].getColumns(singleQueryData);
|
const columns = transformers[transform].getColumns(singleQueryData);
|
||||||
@@ -177,16 +159,6 @@ describe('when transforming time series table', () => {
|
|||||||
expect(columns[3].text).toBe('Value #A');
|
expect(columns[3].text).toBe('Value #A');
|
||||||
expect(columns[4].text).toBe('Value #B');
|
expect(columns[4].text).toBe('Value #B');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the union of data columns given a multiple queries with different labels', () => {
|
|
||||||
const columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels);
|
|
||||||
expect(columns[0].text).toBe('Time');
|
|
||||||
expect(columns[1].text).toBe('Label Key 1');
|
|
||||||
expect(columns[2].text).toBe('Value #A');
|
|
||||||
expect(columns[3].text).toBe('Label Key 2');
|
|
||||||
expect(columns[4].text).toBe('Value #B');
|
|
||||||
expect(columns[5].text).toBe('Value #C');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('transform', () => {
|
describe('transform', () => {
|
||||||
@@ -237,26 +209,6 @@ describe('when transforming time series table', () => {
|
|||||||
expect(table.rows[1][4]).toBeUndefined();
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
expect(table.rows[1][5]).toBe(7);
|
expect(table.rows[1][5]).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 2 rows for multiple queries with different label values', () => {
|
|
||||||
table = transformDataToTable(multipleQueriesDataDifferentLabels, panel);
|
|
||||||
expect(table.rows.length).toBe(2);
|
|
||||||
expect(table.columns.length).toBe(6);
|
|
||||||
|
|
||||||
expect(table.rows[0][0]).toBe(time);
|
|
||||||
expect(table.rows[0][1]).toBe('Label Value 1');
|
|
||||||
expect(table.rows[0][2]).toBe(42);
|
|
||||||
expect(table.rows[0][3]).toBe('Label Value 2');
|
|
||||||
expect(table.rows[0][4]).toBe(13);
|
|
||||||
expect(table.rows[0][5]).toBeUndefined();
|
|
||||||
|
|
||||||
expect(table.rows[1][0]).toBe(time);
|
|
||||||
expect(table.rows[1][1]).toBe('Label Value 3');
|
|
||||||
expect(table.rows[1][2]).toBeUndefined();
|
|
||||||
expect(table.rows[1][3]).toBeUndefined();
|
|
||||||
expect(table.rows[1][4]).toBeUndefined();
|
|
||||||
expect(table.rows[1][5]).toBe(7);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import flatten from '../../../core/utils/flatten';
|
import flatten from 'app/core/utils/flatten';
|
||||||
import TimeSeries from '../../../core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import TableModel from '../../../core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
|
|
||||||
const transformers = {};
|
const transformers = {};
|
||||||
|
|
||||||
@@ -168,97 +168,7 @@ transformers['table'] = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single query returns data columns and rows as is
|
mergeTablesIntoModel(model, ...data);
|
||||||
if (data.length === 1) {
|
|
||||||
model.columns = [...data[0].columns];
|
|
||||||
model.rows = [...data[0].rows];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track column indexes of union: name -> index
|
|
||||||
const columnNames = {};
|
|
||||||
|
|
||||||
// Union of all non-value columns
|
|
||||||
const columnsUnion = data.reduce((acc, series) => {
|
|
||||||
series.columns.forEach(col => {
|
|
||||||
const { text } = col;
|
|
||||||
if (columnNames[text] === undefined) {
|
|
||||||
columnNames[text] = acc.length;
|
|
||||||
acc.push(col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Map old column index to union index per series, e.g.,
|
|
||||||
// given columnNames {A: 0, B: 1} and
|
|
||||||
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
|
||||||
const columnIndexMapper = data.map(series => series.columns.map(col => columnNames[col.text]));
|
|
||||||
|
|
||||||
// Flatten rows of all series and adjust new column indexes
|
|
||||||
const flattenedRows = data.reduce((acc, series, seriesIndex) => {
|
|
||||||
const mapper = columnIndexMapper[seriesIndex];
|
|
||||||
series.rows.forEach(row => {
|
|
||||||
const alteredRow = [];
|
|
||||||
// Shifting entries according to index mapper
|
|
||||||
mapper.forEach((to, from) => {
|
|
||||||
alteredRow[to] = row[from];
|
|
||||||
});
|
|
||||||
acc.push(alteredRow);
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Returns true if both rows have matching non-empty fields as well as matching
|
|
||||||
// indexes where one field is empty and the other is not
|
|
||||||
function areRowsMatching(columns, row, otherRow) {
|
|
||||||
let foundFieldToMatch = false;
|
|
||||||
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
|
||||||
if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
|
|
||||||
if (row[columnIndex] !== otherRow[columnIndex]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
|
|
||||||
foundFieldToMatch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return foundFieldToMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge rows that have same values for columns
|
|
||||||
const mergedRows = {};
|
|
||||||
const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
|
|
||||||
if (!mergedRows[rowIndex]) {
|
|
||||||
// Look from current row onwards
|
|
||||||
let offset = rowIndex + 1;
|
|
||||||
// More than one row can be merged into current row
|
|
||||||
while (offset < flattenedRows.length) {
|
|
||||||
// Find next row that could be merged
|
|
||||||
const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset);
|
|
||||||
if (match > -1) {
|
|
||||||
const matchedRow = flattenedRows[match];
|
|
||||||
// Merge values from match into current row if there is a gap in the current row
|
|
||||||
for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
|
|
||||||
if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
|
|
||||||
row[columnIndex] = matchedRow[columnIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't visit this row again
|
|
||||||
mergedRows[match] = matchedRow;
|
|
||||||
// Keep looking for more rows to merge
|
|
||||||
offset = match + 1;
|
|
||||||
} else {
|
|
||||||
// No match found, stop looking
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
acc.push(row);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
model.columns = columnsUnion;
|
|
||||||
model.rows = compactedRows;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user