grafana/public/app/core/table_model.ts

167 lines
4.8 KiB
TypeScript
Raw Normal View History

import _ from 'lodash';
2019-03-07 14:13:38 -06:00
import { Column, TableData } from '@grafana/ui';
2019-03-11 11:24:25 -05:00
/**
* Extends the standard Column class with variables that get
* mutated in the angular table panel.
*/
2019-03-11 11:29:30 -05:00
interface MutableColumn extends Column {
title?: string;
sort?: boolean;
desc?: boolean;
}
2019-03-07 14:13:38 -06:00
export default class TableModel implements TableData {
2019-03-11 11:29:30 -05:00
columns: MutableColumn[];
2015-11-03 09:19:51 -06:00
rows: any[];
type: string;
columnMap: any;
constructor(table?: any) {
this.columns = [];
this.columnMap = {};
this.rows = [];
2017-12-20 05:33:33 -06:00
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) {
if (options.col === null || this.columns.length <= options.col) {
return;
}
this.rows.sort((a, b) => {
a = a[options.col];
b = b[options.col];
// Sort null or undefined separately from comparable values
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
});
if (options.desc) {
this.rows.reverse();
}
this.columns[options.col].sort = true;
this.columns[options.col].desc = options.desc;
}
addColumn(col) {
if (!this.columnMap[col.text]) {
this.columns.push(col);
this.columnMap[col.text] = col;
}
}
2018-05-30 04:29:44 -05:00
addRow(row) {
this.rows.push(row);
}
2015-11-03 09:19:51 -06:00
}
// 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();
if (arguments.length === 1) {
return model;
}
// Single query returns data columns and rows as is
if (arguments.length === 2) {
model.columns = tables[0].hasOwnProperty('columns') ? [...tables[0].columns] : [];
model.rows = tables[0].hasOwnProperty('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;
}