mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			180 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
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;
 | 
						|
}
 |