From d3f7231a276ede2b620a1b4e0df5127ccd8a844a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Feb 2024 15:25:12 +0100 Subject: [PATCH] DashboardScene: Reload when someone else saves changes (#81855) * DashboardScene: Reload when someone else saves changes * Update public/app/features/dashboard-scene/pages/DashboardScenePage.tsx Co-authored-by: Dominik Prokop * Fixes --------- Co-authored-by: Dominik Prokop --- .betterer.results | 4 +- .../pages/DashboardScenePage.test.tsx | 57 ++++++++++++++++--- .../pages/DashboardScenePage.tsx | 6 +- .../pages/DashboardScenePageStateManager.ts | 5 +- .../dashboard-scene/scene/DashboardScene.tsx | 6 ++ .../DashboardModelCompatibilityWrapper.ts | 4 ++ 6 files changed, 71 insertions(+), 11 deletions(-) diff --git a/.betterer.results b/.betterer.results index 556d0c991c4..e18c7cf5220 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2582,7 +2582,9 @@ exports[`better eslint`] = { [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], "public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index dcd13745076..72107998d74 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -1,5 +1,6 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { cloneDeep } from 'lodash'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; @@ -7,11 +8,12 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { PanelProps } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { config, getPluginLinkExtensions, locationService, setPluginImportUtils } from '@grafana/runtime'; +import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; - -import { setupLoadDashboardMock } from '../utils/test-utils'; +import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DashboardScenePage, Props } from './DashboardScenePage'; +import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -32,7 +34,7 @@ function setup() { const props: Props = { ...getRouteComponentProps(), }; - props.match.params.uid = 'd10'; + props.match.params.uid = 'my-dash-uid'; const renderResult = render( @@ -40,12 +42,22 @@ function setup() { ); - return { renderResult, context }; + const rerender = (newProps: Props) => { + renderResult.rerender( + + + + ); + }; + + return { rerender, context, props }; } -const simpleDashboard = { +const simpleDashboard: Dashboard = { title: 'My cool dashboard', - uid: '10d', + uid: 'my-dash-uid', + schemaVersion: 30, + version: 1, panels: [ { id: 1, @@ -94,10 +106,20 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); +const loadDashboardMock = jest.fn(); + +setDashboardLoaderSrv({ + loadDashboard: loadDashboardMock, + // disabling type checks since this is a test util + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions +} as unknown as DashboardLoaderSrv); + describe('DashboardScenePage', () => { beforeEach(() => { locationService.push('/'); - setupLoadDashboardMock({ dashboard: simpleDashboard, meta: {} }); + getDashboardScenePageStateManager().clearDashboardCache(); + loadDashboardMock.mockClear(); + loadDashboardMock.mockResolvedValue({ dashboard: simpleDashboard, meta: {} }); // hacky way because mocking autosizer does not work Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 }); Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 }); @@ -117,6 +139,27 @@ describe('DashboardScenePage', () => { expect(await screen.findByText('Content B')).toBeInTheDocument(); }); + it('routeReloadCounter should trigger reload', async () => { + const { rerender, props } = setup(); + + await waitForDashbordToRender(); + + expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); + + const updatedDashboard = cloneDeep(simpleDashboard); + updatedDashboard.version = 11; + updatedDashboard.panels![0].title = 'Updated title'; + + getDashboardScenePageStateManager().clearDashboardCache(); + loadDashboardMock.mockResolvedValue({ dashboard: updatedDashboard, meta: {} }); + + props.history.location.state = { routeReloadCounter: 1 }; + + rerender(props); + + expect(await screen.findByTitle('Updated title')).toBeInTheDocument(); + }); + it('Can inspect panel', async () => { setup(); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 19251e72e38..97d683d92ab 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -12,9 +12,11 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana export interface Props extends GrafanaRouteComponentProps {} -export function DashboardScenePage({ match, route, queryParams }: Props) { +export function DashboardScenePage({ match, route, queryParams, history }: Props) { const stateManager = getDashboardScenePageStateManager(); const { dashboard, isLoading, loadError } = stateManager.useState(); + // After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need + const routeReloadCounter = (history.location.state as any)?.routeReloadCounter; useEffect(() => { stateManager.loadDashboard({ @@ -26,7 +28,7 @@ export function DashboardScenePage({ match, route, queryParams }: Props) { return () => { stateManager.clearState(); }; - }, [stateManager, match.params.uid, route.routeName, queryParams.folderUid]); + }, [stateManager, match.params.uid, route.routeName, queryParams.folderUid, routeReloadCounter]); if (!dashboard) { return ( diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 56c0dee4992..3516e56a463 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -80,7 +80,6 @@ export class DashboardScenePageStateManager extends StateManagerBase { this.startTrackingChanges(); } + if (!this.state.meta.isEmbedded && this.state.uid) { + dashboardWatcher.watch(this.state.uid); + } + const clearKeyBindings = setupKeyboardShortcuts(this); const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this); @@ -149,6 +154,7 @@ export class DashboardScene extends SceneObjectBase { this.stopTrackingChanges(); this.stopUrlSync(); oldDashboardWrapper.destroy(); + dashboardWatcher.leave(); }; } diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts index 81f3afa51b3..3bcf30bcc4d 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -228,6 +228,10 @@ export class DashboardModelCompatibilityWrapper { this.events.removeAllListeners(); this._subs.unsubscribe(); } + + public hasUnsavedChanges() { + return this._scene.state.isDirty; + } } class PanelCompatibilityWrapper {