table_model.ts 4.9 KB

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