2018-10-19 08:29:58 -05:00
|
|
|
import _ from 'lodash';
|
2020-05-11 15:47:15 -05:00
|
|
|
import { Column, TableData, QueryResultMeta } from '@grafana/data';
|
2018-10-19 08:29:58 -05:00
|
|
|
|
2019-03-11 11:24:25 -05:00
|
|
|
/**
|
|
|
|
* Extends the standard Column class with variables that get
|
|
|
|
* mutated in the angular table panel.
|
|
|
|
*/
|
2020-05-13 08:34:23 -05:00
|
|
|
export interface MutableColumn extends Column {
|
2018-08-03 03:20:13 -05:00
|
|
|
title?: string;
|
|
|
|
sort?: boolean;
|
|
|
|
desc?: boolean;
|
2019-03-25 08:46:16 -05:00
|
|
|
type?: string;
|
2018-08-03 03:20:13 -05:00
|
|
|
}
|
|
|
|
|
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[];
|
2015-12-03 08:09:39 -06:00
|
|
|
type: string;
|
2017-08-02 05:15:39 -05:00
|
|
|
columnMap: any;
|
2020-07-08 04:05:20 -05:00
|
|
|
refId?: string;
|
2020-05-11 15:47:15 -05:00
|
|
|
meta?: QueryResultMeta;
|
2015-11-02 13:51:49 -06:00
|
|
|
|
2018-10-19 08:29:58 -05:00
|
|
|
constructor(table?: any) {
|
2015-11-05 01:36:51 -06:00
|
|
|
this.columns = [];
|
2017-08-02 05:15:39 -05:00
|
|
|
this.columnMap = {};
|
2015-11-05 01:36:51 -06:00
|
|
|
this.rows = [];
|
2017-12-20 05:33:33 -06:00
|
|
|
this.type = 'table';
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
if (table) {
|
|
|
|
if (table.columns) {
|
2019-05-13 02:38:19 -05:00
|
|
|
for (const col of table.columns) {
|
|
|
|
this.addColumn(col);
|
|
|
|
}
|
2018-10-19 08:29:58 -05:00
|
|
|
}
|
|
|
|
if (table.rows) {
|
2019-05-13 02:38:19 -05:00
|
|
|
for (const row of table.rows) {
|
|
|
|
this.addRow(row);
|
|
|
|
}
|
2018-10-19 08:29:58 -05:00
|
|
|
}
|
|
|
|
}
|
2015-11-05 01:36:51 -06:00
|
|
|
}
|
|
|
|
|
2019-05-13 02:38:19 -05:00
|
|
|
sort(options: { col: number; desc: boolean }) {
|
2015-11-12 05:39:16 -06:00
|
|
|
if (options.col === null || this.columns.length <= options.col) {
|
2015-11-09 10:58:02 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-09-04 10:02:32 -05:00
|
|
|
this.rows.sort((a, b) => {
|
2015-11-09 10:58:02 -06:00
|
|
|
a = a[options.col];
|
|
|
|
b = b[options.col];
|
2018-12-05 06:02:58 -06:00
|
|
|
// Sort null or undefined separately from comparable values
|
2018-07-02 13:14:41 -05:00
|
|
|
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
|
2015-11-09 10:58:02 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
if (options.desc) {
|
|
|
|
this.rows.reverse();
|
|
|
|
}
|
2018-07-02 13:14:41 -05:00
|
|
|
|
|
|
|
this.columns[options.col].sort = true;
|
|
|
|
this.columns[options.col].desc = options.desc;
|
2015-11-09 10:58:02 -06:00
|
|
|
}
|
2017-08-02 05:15:39 -05:00
|
|
|
|
2019-05-13 02:38:19 -05:00
|
|
|
addColumn(col: Column) {
|
2017-08-02 05:15:39 -05:00
|
|
|
if (!this.columnMap[col.text]) {
|
|
|
|
this.columns.push(col);
|
|
|
|
this.columnMap[col.text] = col;
|
|
|
|
}
|
|
|
|
}
|
2018-05-30 04:29:44 -05:00
|
|
|
|
2019-05-13 02:38:19 -05:00
|
|
|
addRow(row: any[]) {
|
2018-05-30 04:29:44 -05:00
|
|
|
this.rows.push(row);
|
|
|
|
}
|
2015-11-03 09:19:51 -06:00
|
|
|
}
|
2018-10-19 08:29:58 -05: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
|
2019-05-13 02:38:19 -05:00
|
|
|
function areRowsMatching(columns: Column[], row: any[], otherRow: any[]) {
|
2018-10-19 08:29:58 -05:00
|
|
|
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();
|
|
|
|
|
2018-10-26 11:16:00 -05:00
|
|
|
if (arguments.length === 1) {
|
|
|
|
return model;
|
|
|
|
}
|
2018-10-19 08:29:58 -05:00
|
|
|
// Single query returns data columns and rows as is
|
|
|
|
if (arguments.length === 2) {
|
2018-11-28 07:31:20 -06:00
|
|
|
model.columns = tables[0].hasOwnProperty('columns') ? [...tables[0].columns] : [];
|
|
|
|
model.rows = tables[0].hasOwnProperty('rows') ? [...tables[0].rows] : [];
|
2018-10-19 08:29:58 -05:00
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
2019-11-06 09:57:15 -06:00
|
|
|
// Filter out any tables that are not of TableData format
|
2021-01-20 00:59:48 -06:00
|
|
|
const tableDataTables = tables.filter((table) => !!table.columns);
|
2019-11-06 09:57:15 -06:00
|
|
|
|
2018-10-19 08:29:58 -05:00
|
|
|
// Track column indexes of union: name -> index
|
2019-05-13 02:38:19 -05:00
|
|
|
const columnNames: { [key: string]: any } = {};
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
// Union of all non-value columns
|
2019-11-19 07:59:39 -06:00
|
|
|
const columnsUnion = tableDataTables.slice().reduce((acc, series) => {
|
2021-01-20 00:59:48 -06:00
|
|
|
series.columns.forEach((col) => {
|
2019-11-19 07:59:39 -06:00
|
|
|
const { text } = col;
|
|
|
|
if (columnNames[text] === undefined) {
|
|
|
|
columnNames[text] = acc.length;
|
|
|
|
acc.push(col);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return acc;
|
|
|
|
}, [] as MutableColumn[]);
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
// 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]]
|
2021-01-20 00:59:48 -06:00
|
|
|
const columnIndexMapper = tableDataTables.map((series) => series.columns.map((col) => columnNames[col.text]));
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
// Flatten rows of all series and adjust new column indexes
|
2019-11-19 07:59:39 -06:00
|
|
|
const flattenedRows = tableDataTables.reduce((acc, series, seriesIndex) => {
|
|
|
|
const mapper = columnIndexMapper[seriesIndex];
|
2021-01-20 00:59:48 -06:00
|
|
|
series.rows.forEach((row) => {
|
2019-11-19 07:59:39 -06:00
|
|
|
const alteredRow: MutableColumn[] = [];
|
|
|
|
// Shifting entries according to index mapper
|
|
|
|
mapper.forEach((to, from) => {
|
|
|
|
alteredRow[to] = row[from];
|
2018-10-19 08:29:58 -05:00
|
|
|
});
|
2019-11-19 07:59:39 -06:00
|
|
|
acc.push(alteredRow);
|
|
|
|
});
|
|
|
|
return acc;
|
|
|
|
}, [] as MutableColumn[][]);
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
// Merge rows that have same values for columns
|
2019-05-13 02:38:19 -05:00
|
|
|
const mergedRows: { [key: string]: any } = {};
|
|
|
|
|
2019-11-19 07:59:39 -06:00
|
|
|
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
|
2021-01-20 00:59:48 -06:00
|
|
|
const match = _.findIndex(flattenedRows, (otherRow) => areRowsMatching(columnsUnion, row, otherRow), offset);
|
2019-11-19 07:59:39 -06:00
|
|
|
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];
|
2018-10-19 08:29:58 -05:00
|
|
|
}
|
|
|
|
}
|
2019-11-19 07:59:39 -06:00
|
|
|
// 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;
|
2018-10-19 08:29:58 -05:00
|
|
|
}
|
|
|
|
}
|
2019-11-19 07:59:39 -06:00
|
|
|
acc.push(row);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, [] as MutableColumn[][]);
|
2018-10-19 08:29:58 -05:00
|
|
|
|
|
|
|
model.columns = columnsUnion;
|
|
|
|
model.rows = compactedRows;
|
|
|
|
return model;
|
|
|
|
}
|