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 <dominik.prokop@grafana.com>

* Fixes

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard
2024-02-05 15:25:12 +01:00
committed by GitHub
parent 8f65e36b06
commit d3f7231a27
6 changed files with 71 additions and 11 deletions

View File

@@ -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"]

View File

@@ -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(
<TestProvider grafanaContext={context}>
@@ -40,12 +42,22 @@ function setup() {
</TestProvider>
);
return { renderResult, context };
const rerender = (newProps: Props) => {
renderResult.rerender(
<TestProvider grafanaContext={context}>
<DashboardScenePage {...newProps} />
</TestProvider>
);
};
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();

View File

@@ -12,9 +12,11 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> {}
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 (

View File

@@ -80,7 +80,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
break;
default:
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
if (route === DashboardRoutes.Embedded) {
rsp.meta.isEmbedded = true;
}
@@ -188,6 +187,10 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) {
this.dashboardCache = { dashboard, ts: Date.now(), cacheKey };
}
public clearDashboardCache() {
this.dashboardCache = undefined;
}
}
let stateManager: DashboardScenePageStateManager | null = null;

View File

@@ -23,6 +23,7 @@ import appEvents from 'app/core/app_events';
import { getNavModel } from 'app/core/selectors/navModel';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
@@ -136,6 +137,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
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<DashboardSceneState> {
this.stopTrackingChanges();
this.stopUrlSync();
oldDashboardWrapper.destroy();
dashboardWatcher.leave();
};
}

View File

@@ -228,6 +228,10 @@ export class DashboardModelCompatibilityWrapper {
this.events.removeAllListeners();
this._subs.unsubscribe();
}
public hasUnsavedChanges() {
return this._scene.state.isDirty;
}
}
class PanelCompatibilityWrapper {