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: any;
 | |
|   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: any[]) {
 | |
|     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: any[], otherRow: any[]) {
 | |
|   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: { [key: string]: any } = {};
 | |
| 
 | |
|   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;
 | |
| }
 |