From 06115478f9647dade240b54a6c5958c6dc6b2626 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:03:24 -0400 Subject: [PATCH] fix: dashboard version history duplicating entries sometimes (#109490) * fix: version history duplicating entries when navigation fails after restoring dashboard version * fix navigation issue --------- Co-authored-by: Haris Rozajac --- .../scene/DashboardScene.test.tsx | 48 ++++++++++++++++++- .../settings/VersionsEditView.tsx | 2 +- .../live/dashboard/dashboardWatcher.ts | 10 +++- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 214a47abc27..84963900760 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,4 +1,12 @@ -import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data'; +import { + CoreApp, + GrafanaConfig, + LiveChannelEventType, + LoadingState, + getDefaultTimeRange, + locationUtil, + store, +} from '@grafana/data'; import { config, locationService, RefreshEvent } from '@grafana/runtime'; import { sceneGraph, @@ -17,6 +25,8 @@ import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { DashboardEventAction } from 'app/features/live/dashboard/types'; import { VariablesChanged } from 'app/features/variables/types'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -764,12 +774,46 @@ describe('DashboardScene', () => { return scene.onRestore(getVersionMock()).then((res) => { expect(res).toBe(true); - expect(scene.state.version).toBe(newVersion); expect(scene.state.isEditing).toBe(false); }); }); + it('should call dashboardWatcher.reloadPage even if dashboard is in editing mode', async () => { + // sometimes a dashboard can be in editing mode after user has already restored to a previous version + + const newVersion = 3; + const mockScene = new DashboardScene({ + title: 'new name', + uid: 'dash-1', + version: 4, + }); + jest.mocked(historySrv.restoreDashboard).mockResolvedValue({ version: newVersion }); + jest.mocked(transformSaveModelToScene).mockReturnValue(mockScene); + + const reloadSpy = jest.spyOn(dashboardWatcher, 'reloadPage').mockImplementation(() => {}); + + dashboardWatcher.editing = false; + const dash = { uid: 'dash-1', hasUnsavedChanges: () => true }; + jest + .spyOn(require('app/features/dashboard/services/DashboardSrv'), 'getDashboardSrv') + .mockReturnValue({ getCurrent: () => dash }); + + dashboardWatcher.observer.next({ + type: LiveChannelEventType.Message, + message: { + sessionId: 'other', + message: 'Restored from version 3', + uid: 'dash-1', + action: DashboardEventAction.Saved, + timestamp: Date.now(), + }, + }); + + expect(reloadSpy).toHaveBeenCalled(); + reloadSpy.mockRestore(); + }); + it('should return early if historySrv does not return a valid version number', () => { jest .mocked(historySrv.restoreDashboard) diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index e21c7c00cf1..2f194784bc8 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -113,7 +113,7 @@ export class VersionsEditView extends SceneObjectBase imp .then((result) => { this.setState({ isLoading: false, - versions: [...(this.state.versions ?? []), ...this.decorateVersions(result.versions)], + versions: [...(append ? (this.state.versions ?? []) : []), ...this.decorateVersions(result.versions)], }); this._start += this._limit; // Update the continueToken for the next request, if available diff --git a/public/app/features/live/dashboard/dashboardWatcher.ts b/public/app/features/live/dashboard/dashboardWatcher.ts index 3572a77011f..5c831085af5 100644 --- a/public/app/features/live/dashboard/dashboardWatcher.ts +++ b/public/app/features/live/dashboard/dashboardWatcher.ts @@ -109,7 +109,7 @@ class DashboardWatcher { return; // skip internal messages } - const { action } = event.message; + const { action, message } = event.message; switch (action) { case DashboardEventAction.EditingStarted: case DashboardEventAction.Saved: { @@ -124,7 +124,13 @@ class DashboardWatcher { return; } - const showPopup = this.editing || dash.hasUnsavedChanges(); + let showPopup = this.editing || dash.hasUnsavedChanges(); + + // Dashboard could have unsaved changes but if user has already restored from a version + // the reloadPage should be called below + if (message?.includes('Restored from version')) { + showPopup = false; + } if (action === DashboardEventAction.Saved) { if (showPopup) {