change_tracker.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import angular from 'angular';
  2. import _ from 'lodash';
  3. import { DashboardModel } from './dashboard_model';
  4. export class ChangeTracker {
  5. current: any;
  6. originalPath: any;
  7. scope: any;
  8. original: any;
  9. next: any;
  10. $window: any;
  11. /** @ngInject */
  12. constructor(
  13. dashboard,
  14. scope,
  15. originalCopyDelay,
  16. private $location,
  17. $window,
  18. private $timeout,
  19. private contextSrv,
  20. private $rootScope
  21. ) {
  22. this.$location = $location;
  23. this.$window = $window;
  24. this.current = dashboard;
  25. this.originalPath = $location.path();
  26. this.scope = scope;
  27. // register events
  28. scope.onAppEvent('dashboard-saved', () => {
  29. this.original = this.current.getSaveModelClone();
  30. this.originalPath = $location.path();
  31. });
  32. $window.onbeforeunload = () => {
  33. if (this.ignoreChanges()) {
  34. return undefined;
  35. }
  36. if (this.hasChanges()) {
  37. return 'There are unsaved changes to this dashboard';
  38. }
  39. return undefined;
  40. };
  41. scope.$on('$locationChangeStart', (event, next) => {
  42. // check if we should look for changes
  43. if (this.originalPath === $location.path()) {
  44. return true;
  45. }
  46. if (this.ignoreChanges()) {
  47. return true;
  48. }
  49. if (this.hasChanges()) {
  50. event.preventDefault();
  51. this.next = next;
  52. this.$timeout(() => {
  53. this.open_modal();
  54. });
  55. }
  56. return false;
  57. });
  58. if (originalCopyDelay) {
  59. this.$timeout(() => {
  60. // wait for different services to patch the dashboard (missing properties)
  61. this.original = dashboard.getSaveModelClone();
  62. }, originalCopyDelay);
  63. } else {
  64. this.original = dashboard.getSaveModelClone();
  65. }
  66. }
  67. // for some dashboards and users
  68. // changes should be ignored
  69. ignoreChanges() {
  70. if (!this.original) {
  71. return true;
  72. }
  73. if (!this.contextSrv.isEditor) {
  74. return true;
  75. }
  76. if (!this.current || !this.current.meta) {
  77. return true;
  78. }
  79. const meta = this.current.meta;
  80. return !meta.canSave || meta.fromScript || meta.fromFile;
  81. }
  82. // remove stuff that should not count in diff
  83. cleanDashboardFromIgnoredChanges(dashData) {
  84. // need to new up the domain model class to get access to expand / collapse row logic
  85. const model = new DashboardModel(dashData);
  86. // Expand all rows before making comparison. This is required because row expand / collapse
  87. // change order of panel array and panel positions.
  88. model.expandRows();
  89. const dash = model.getSaveModelClone();
  90. // ignore time and refresh
  91. dash.time = 0;
  92. dash.refresh = 0;
  93. dash.schemaVersion = 0;
  94. // ignore iteration property
  95. delete dash.iteration;
  96. dash.panels = _.filter(dash.panels, panel => {
  97. if (panel.repeatPanelId) {
  98. return false;
  99. }
  100. // remove scopedVars
  101. panel.scopedVars = null;
  102. // ignore panel legend sort
  103. if (panel.legend) {
  104. delete panel.legend.sort;
  105. delete panel.legend.sortDesc;
  106. }
  107. return true;
  108. });
  109. // ignore template variable values
  110. _.each(dash.templating.list, value => {
  111. value.current = null;
  112. value.options = null;
  113. value.filters = null;
  114. });
  115. return dash;
  116. }
  117. hasChanges() {
  118. const current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone());
  119. const original = this.cleanDashboardFromIgnoredChanges(this.original);
  120. const currentTimepicker = _.find(current.nav, { type: 'timepicker' });
  121. const originalTimepicker = _.find(original.nav, { type: 'timepicker' });
  122. if (currentTimepicker && originalTimepicker) {
  123. currentTimepicker.now = originalTimepicker.now;
  124. }
  125. const currentJson = angular.toJson(current, true);
  126. const originalJson = angular.toJson(original, true);
  127. return currentJson !== originalJson;
  128. }
  129. discardChanges() {
  130. this.original = null;
  131. this.gotoNext();
  132. }
  133. open_modal() {
  134. this.$rootScope.appEvent('show-modal', {
  135. templateHtml: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
  136. modalClass: 'modal--narrow confirm-modal',
  137. });
  138. }
  139. saveChanges() {
  140. const self = this;
  141. const cancel = this.$rootScope.$on('dashboard-saved', () => {
  142. cancel();
  143. this.$timeout(() => {
  144. self.gotoNext();
  145. });
  146. });
  147. this.$rootScope.appEvent('save-dashboard');
  148. }
  149. gotoNext() {
  150. const baseLen = this.$location.absUrl().length - this.$location.url().length;
  151. const nextUrl = this.next.substring(baseLen);
  152. this.$location.url(nextUrl);
  153. }
  154. }