import angular, { ILocationService, IRootScopeService } from 'angular'; import _ from 'lodash'; import { DashboardModel } from '../state/DashboardModel'; import { ContextSrv } from 'app/core/services/context_srv'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { AppEventConsumer, CoreEvents } from 'app/types'; import { appEvents } from 'app/core/app_events'; import { UnsavedChangesModal } from '../components/SaveDashboard/UnsavedChangesModal'; export class ChangeTracker { current: any; originalPath: any; scope: any; original: any; next: any; $window: any; /** @ngInject */ constructor( dashboard: DashboardModel, scope: IRootScopeService & AppEventConsumer, originalCopyDelay: any, private $location: ILocationService, $window: any, private $timeout: any, private contextSrv: ContextSrv, private $rootScope: GrafanaRootScope ) { this.$location = $location; this.$window = $window; this.current = dashboard; this.originalPath = $location.path(); this.scope = scope; // register events appEvents.on(CoreEvents.dashboardSaved, () => { this.original = this.current.getSaveModelClone(); this.originalPath = $location.path(); }); $window.onbeforeunload = () => { if (this.ignoreChanges()) { return undefined; } if (this.hasChanges()) { return 'There are unsaved changes to this dashboard'; } return undefined; }; scope.$on('$locationChangeStart', (event: any, next: any) => { // check if we should look for changes if (this.originalPath === $location.path()) { return true; } if (this.ignoreChanges()) { return true; } if (this.hasChanges()) { event.preventDefault(); this.next = next; this.$timeout(() => { this.open_modal(); }); } return false; }); if (originalCopyDelay && !dashboard.meta.fromExplore) { this.$timeout(() => { // wait for different services to patch the dashboard (missing properties) this.original = dashboard.getSaveModelClone(); }, originalCopyDelay); } else { this.original = dashboard.getSaveModelClone(); } } // for some dashboards and users // changes should be ignored ignoreChanges() { if (!this.original) { return true; } if (!this.contextSrv.isEditor) { return true; } if (!this.current || !this.current.meta) { return true; } const meta = this.current.meta; return !meta.canSave || meta.fromScript || meta.fromFile; } // remove stuff that should not count in diff cleanDashboardFromIgnoredChanges(dashData: any) { // need to new up the domain model class to get access to expand / collapse row logic const model = new DashboardModel(dashData); // Expand all rows before making comparison. This is required because row expand / collapse // change order of panel array and panel positions. model.expandRows(); const dash = model.getSaveModelClone(); // ignore time and refresh dash.time = 0; dash.refresh = 0; dash.schemaVersion = 0; // ignore iteration property delete dash.iteration; dash.panels = _.filter(dash.panels, panel => { if (panel.repeatPanelId) { return false; } // remove scopedVars panel.scopedVars = null; // ignore panel legend sort if (panel.legend) { delete panel.legend.sort; delete panel.legend.sortDesc; } return true; }); // ignore template variable values _.each(dash.getVariables(), (variable: any) => { variable.current = null; variable.options = null; variable.filters = null; }); return dash; } hasChanges() { const current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); const original = this.cleanDashboardFromIgnoredChanges(this.original); const currentTimepicker: any = _.find((current as any).nav, { type: 'timepicker' }); const originalTimepicker: any = _.find((original as any).nav, { type: 'timepicker' }); if (currentTimepicker && originalTimepicker) { currentTimepicker.now = originalTimepicker.now; } const currentJson = angular.toJson(current, true); const originalJson = angular.toJson(original, true); return currentJson !== originalJson; } discardChanges = () => { this.original = null; this.gotoNext(); }; open_modal = () => { this.$rootScope.appEvent(CoreEvents.showModalReact, { component: UnsavedChangesModal, props: { dashboard: this.current, onSaveSuccess: this.onSaveSuccess, onDiscard: () => { this.discardChanges(); }, }, }); }; onSaveSuccess = () => { this.$timeout(() => { this.gotoNext(); }); }; gotoNext = () => { const baseLen = this.$location.absUrl().length - this.$location.url().length; const nextUrl = this.next.substring(baseLen); this.$timeout(() => { this.$location.url(nextUrl); }); }; }