table_model.ts 4.8 KB

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