panelMerge.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { isEqualWith } from 'lodash';
  2. import { PanelModel as IPanelModel } from '@grafana/data';
  3. import { PanelModel } from '../state';
  4. export interface PanelMergeInfo {
  5. changed: boolean;
  6. panels: PanelModel[];
  7. actions: Record<string, number[]>;
  8. }
  9. // Values that are safe to change without a full panel unmount/remount
  10. // TODO: options and fieldConfig should also be supported
  11. const mutableKeys = new Set<keyof PanelModel>(['gridPos', 'title', 'description', 'transparent']);
  12. export function mergePanels(current: PanelModel[], data: IPanelModel[]): PanelMergeInfo {
  13. const panels: PanelModel[] = [];
  14. const info = {
  15. changed: false,
  16. actions: {
  17. add: [] as number[],
  18. remove: [] as number[],
  19. replace: [] as number[],
  20. update: [] as number[],
  21. noop: [] as number[],
  22. },
  23. panels,
  24. };
  25. let nextId = 0;
  26. const inputPanels = new Map<number, IPanelModel>();
  27. for (let p of data) {
  28. let { id } = p;
  29. if (!id) {
  30. if (!nextId) {
  31. nextId = findNextPanelID([current, data]);
  32. }
  33. id = nextId++;
  34. p = { ...p, id }; // clone with new ID
  35. }
  36. inputPanels.set(id, p);
  37. }
  38. for (const panel of current) {
  39. const target = inputPanels.get(panel.id) as PanelModel;
  40. if (!target) {
  41. info.changed = true;
  42. info.actions.remove.push(panel.id);
  43. panel.destroy();
  44. continue;
  45. }
  46. inputPanels.delete(panel.id);
  47. // Fast comparison when working with the same panel objects
  48. if (target === panel) {
  49. panels.push(panel);
  50. info.actions.noop.push(panel.id);
  51. continue;
  52. }
  53. // Check if it is the same type
  54. if (panel.type === target.type) {
  55. const save = panel.getSaveModel();
  56. let isNoop = true;
  57. let doUpdate = false;
  58. for (const [key, value] of Object.entries(target)) {
  59. if (!isEqualWith(value, save[key], infinityEqualsNull)) {
  60. info.changed = true;
  61. isNoop = false;
  62. if (mutableKeys.has(key as any)) {
  63. (panel as any)[key] = value;
  64. doUpdate = true;
  65. } else {
  66. doUpdate = false;
  67. break; // needs full replace
  68. }
  69. }
  70. }
  71. if (isNoop) {
  72. panels.push(panel);
  73. info.actions.noop.push(panel.id);
  74. continue;
  75. }
  76. if (doUpdate) {
  77. panels.push(panel);
  78. info.actions.update.push(panel.id);
  79. continue;
  80. }
  81. }
  82. panel.destroy();
  83. const next = new PanelModel(target);
  84. next.key = `${next.id}-update-${Date.now()}`; // force react invalidate
  85. panels.push(next);
  86. info.changed = true;
  87. info.actions.replace.push(panel.id);
  88. }
  89. // Add the new panels
  90. for (const t of inputPanels.values()) {
  91. panels.push(new PanelModel(t));
  92. info.changed = true;
  93. info.actions.add.push(t.id);
  94. }
  95. return info;
  96. }
  97. // Since +- Infinity are saved as null in JSON, we need to make them equal here also
  98. function infinityEqualsNull(a: any, b: any) {
  99. if (a == null && (b === Infinity || b === -Infinity || b == null)) {
  100. return true;
  101. }
  102. if (b == null && (a === Infinity || a === -Infinity || a == null)) {
  103. return true;
  104. }
  105. return undefined; // use default comparison
  106. }
  107. function findNextPanelID(args: IPanelModel[][]): number {
  108. let max = 0;
  109. for (const panels of args) {
  110. for (const panel of panels) {
  111. if (panel.id > max) {
  112. max = panel.id;
  113. }
  114. }
  115. }
  116. return max + 1;
  117. }