ChangeTracker.ts 4.7 KB

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