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 <haris.rozajac12@gmail.com>
This commit is contained in:
Will Assis
2025-08-19 07:03:24 -04:00
committed by GitHub
co-authored by Haris Rozajac
parent 2e5b55a855
commit 06115478f9
3 changed files with 55 additions and 5 deletions
@@ -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)
@@ -113,7 +113,7 @@ export class VersionsEditView extends SceneObjectBase<VersionsEditViewState> 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
@@ -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) {