The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/core/TableModel.ts

179 lines
5.5 KiB

import { findIndex } from 'lodash';
import { Column, TableData, QueryResultMeta } from '@grafana/data';
/**
* Extends the standard Column class with variables that get
* mutated in the angular table panel.
*/
export interface MutableColumn extends Column {
title?: string;
sort?: boolean;
desc?: boolean;
type?: string;
}
export default class TableModel implements TableData {
columns: MutableColumn[];
rows: any[];
type: string;
columnMap: Record<string, Column>;
refId?: string;
meta?: QueryResultMeta;
constructor(table?: any) {
this.columns = [];
this.columnMap = {};
this.rows = [];
this.type = 'table';
if (table) {
if (table.columns) {
for (const col of table.columns) {
this.addColumn(col);
}
}
if (table.rows) {
for (const row of table.rows) {
this.addRow(row);
}
}
}
}
sort(options: { col: number; desc: boolean }) {
// Since 8.3.0 col property can be also undefined, https://github.com/grafana/grafana/issues/44127
if (options.col === null || options.col === undefined || 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: Column) {
if (!this.columnMap[col.text]) {
this.columns.push(col);
this.columnMap[col.text] = col;
}
}
addRow(row: unknown[]) {
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: Column[], row: unknown[], otherRow: unknown[]) {
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;
}
// Filter out any tables that are not of TableData format
const tableDataTables = tables.filter((table) => !!table.columns);
// Track column indexes of union: name -> index
const columnNames: { [key: string]: number } = {};
// Union of all non-value columns
const columnsUnion = tableDataTables.slice().reduce<MutableColumn[]>((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 = tableDataTables.map((series) => series.columns.map((col) => columnNames[col.text]));
// Flatten rows of all series and adjust new column indexes
const flattenedRows = tableDataTables.reduce<MutableColumn[][]>((acc, series, seriesIndex) => {
const mapper = columnIndexMapper[seriesIndex];
series.rows.forEach((row) => {
const alteredRow: MutableColumn[] = [];
// 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: Record<number, MutableColumn[]> = {};
const compactedRows = flattenedRows.reduce<MutableColumn[][]>((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;
}