table_model.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import { findIndex } from 'lodash';
  2. import { Column, TableData, QueryResultMeta } from '@grafana/data';
  3. /**
  4. * Extends the standard Column class with variables that get
  5. * mutated in the angular table panel.
  6. */
  7. export interface MutableColumn extends Column {
  8. title?: string;
  9. sort?: boolean;
  10. desc?: boolean;
  11. type?: string;
  12. }
  13. export default class TableModel implements TableData {
  14. columns: MutableColumn[];
  15. rows: any[];
  16. type: string;
  17. columnMap: any;
  18. refId?: string;
  19. meta?: QueryResultMeta;
  20. constructor(table?: any) {
  21. this.columns = [];
  22. this.columnMap = {};
  23. this.rows = [];
  24. this.type = 'table';
  25. if (table) {
  26. if (table.columns) {
  27. for (const col of table.columns) {
  28. this.addColumn(col);
  29. }
  30. }
  31. if (table.rows) {
  32. for (const row of table.rows) {
  33. this.addRow(row);
  34. }
  35. }
  36. }
  37. }
  38. sort(options: { col: number; desc: boolean }) {
  39. // Since 8.3.0 col property can be also undefined, https://github.com/grafana/grafana/issues/44127
  40. if (options.col === null || options.col === undefined || this.columns.length <= options.col) {
  41. return;
  42. }
  43. this.rows.sort((a, b) => {
  44. a = a[options.col];
  45. b = b[options.col];
  46. // Sort null or undefined separately from comparable values
  47. return +(a == null) - +(b == null) || +(a > b) || -(a < b);
  48. });
  49. if (options.desc) {
  50. this.rows.reverse();
  51. }
  52. this.columns[options.col].sort = true;
  53. this.columns[options.col].desc = options.desc;
  54. }
  55. addColumn(col: Column) {
  56. if (!this.columnMap[col.text]) {
  57. this.columns.push(col);
  58. this.columnMap[col.text] = col;
  59. }
  60. }
  61. addRow(row: any[]) {
  62. this.rows.push(row);
  63. }
  64. }
  65. // Returns true if both rows have matching non-empty fields as well as matching
  66. // indexes where one field is empty and the other is not
  67. function areRowsMatching(columns: Column[], row: any[], otherRow: any[]) {
  68. let foundFieldToMatch = false;
  69. for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
  70. if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
  71. if (row[columnIndex] !== otherRow[columnIndex]) {
  72. return false;
  73. }
  74. } else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
  75. foundFieldToMatch = true;
  76. }
  77. }
  78. return foundFieldToMatch;
  79. }
  80. export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
  81. const model = dst || new TableModel();
  82. if (arguments.length === 1) {
  83. return model;
  84. }
  85. // Single query returns data columns and rows as is
  86. if (arguments.length === 2) {
  87. model.columns = tables[0].hasOwnProperty('columns') ? [...tables[0].columns] : [];
  88. model.rows = tables[0].hasOwnProperty('rows') ? [...tables[0].rows] : [];
  89. return model;
  90. }
  91. // Filter out any tables that are not of TableData format
  92. const tableDataTables = tables.filter((table) => !!table.columns);
  93. // Track column indexes of union: name -> index
  94. const columnNames: { [key: string]: any } = {};
  95. // Union of all non-value columns
  96. const columnsUnion = tableDataTables.slice().reduce((acc, series) => {
  97. series.columns.forEach((col) => {
  98. const { text } = col;
  99. if (columnNames[text] === undefined) {
  100. columnNames[text] = acc.length;
  101. acc.push(col);
  102. }
  103. });
  104. return acc;
  105. }, [] as MutableColumn[]);
  106. // Map old column index to union index per series, e.g.,
  107. // given columnNames {A: 0, B: 1} and
  108. // data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
  109. const columnIndexMapper = tableDataTables.map((series) => series.columns.map((col) => columnNames[col.text]));
  110. // Flatten rows of all series and adjust new column indexes
  111. const flattenedRows = tableDataTables.reduce((acc, series, seriesIndex) => {
  112. const mapper = columnIndexMapper[seriesIndex];
  113. series.rows.forEach((row) => {
  114. const alteredRow: MutableColumn[] = [];
  115. // Shifting entries according to index mapper
  116. mapper.forEach((to, from) => {
  117. alteredRow[to] = row[from];
  118. });
  119. acc.push(alteredRow);
  120. });
  121. return acc;
  122. }, [] as MutableColumn[][]);
  123. // Merge rows that have same values for columns
  124. const mergedRows: { [key: string]: any } = {};
  125. const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
  126. if (!mergedRows[rowIndex]) {
  127. // Look from current row onwards
  128. let offset = rowIndex + 1;
  129. // More than one row can be merged into current row
  130. while (offset < flattenedRows.length) {
  131. // Find next row that could be merged
  132. const match = findIndex(flattenedRows, (otherRow) => areRowsMatching(columnsUnion, row, otherRow), offset);
  133. if (match > -1) {
  134. const matchedRow = flattenedRows[match];
  135. // Merge values from match into current row if there is a gap in the current row
  136. for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
  137. if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
  138. row[columnIndex] = matchedRow[columnIndex];
  139. }
  140. }
  141. // Don't visit this row again
  142. mergedRows[match] = matchedRow;
  143. // Keep looking for more rows to merge
  144. offset = match + 1;
  145. } else {
  146. // No match found, stop looking
  147. break;
  148. }
  149. }
  150. acc.push(row);
  151. }
  152. return acc;
  153. }, [] as MutableColumn[][]);
  154. model.columns = columnsUnion;
  155. model.rows = compactedRows;
  156. return model;
  157. }