mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -2582,7 +2582,9 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [
|
"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": [
|
"public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
@@ -7,11 +8,12 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
|||||||
import { PanelProps } from '@grafana/data';
|
import { PanelProps } from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||||
import { config, getPluginLinkExtensions, locationService, setPluginImportUtils } from '@grafana/runtime';
|
import { config, getPluginLinkExtensions, locationService, setPluginImportUtils } from '@grafana/runtime';
|
||||||
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
|
import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
|
||||||
|
|
||||||
import { DashboardScenePage, Props } from './DashboardScenePage';
|
import { DashboardScenePage, Props } from './DashboardScenePage';
|
||||||
|
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
@@ -32,7 +34,7 @@ function setup() {
|
|||||||
const props: Props = {
|
const props: Props = {
|
||||||
...getRouteComponentProps(),
|
...getRouteComponentProps(),
|
||||||
};
|
};
|
||||||
props.match.params.uid = 'd10';
|
props.match.params.uid = 'my-dash-uid';
|
||||||
|
|
||||||
const renderResult = render(
|
const renderResult = render(
|
||||||
<TestProvider grafanaContext={context}>
|
<TestProvider grafanaContext={context}>
|
||||||
@@ -40,12 +42,22 @@ function setup() {
|
|||||||
</TestProvider>
|
</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',
|
title: 'My cool dashboard',
|
||||||
uid: '10d',
|
uid: 'my-dash-uid',
|
||||||
|
schemaVersion: 30,
|
||||||
|
version: 1,
|
||||||
panels: [
|
panels: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -94,10 +106,20 @@ setPluginImportUtils({
|
|||||||
getPanelPluginFromCache: (id: string) => undefined,
|
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', () => {
|
describe('DashboardScenePage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
locationService.push('/');
|
locationService.push('/');
|
||||||
setupLoadDashboardMock({ dashboard: simpleDashboard, meta: {} });
|
getDashboardScenePageStateManager().clearDashboardCache();
|
||||||
|
loadDashboardMock.mockClear();
|
||||||
|
loadDashboardMock.mockResolvedValue({ dashboard: simpleDashboard, meta: {} });
|
||||||
// hacky way because mocking autosizer does not work
|
// hacky way because mocking autosizer does not work
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { 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();
|
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 () => {
|
it('Can inspect panel', async () => {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana
|
|||||||
|
|
||||||
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> {}
|
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 stateManager = getDashboardScenePageStateManager();
|
||||||
const { dashboard, isLoading, loadError } = stateManager.useState();
|
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(() => {
|
useEffect(() => {
|
||||||
stateManager.loadDashboard({
|
stateManager.loadDashboard({
|
||||||
@@ -26,7 +28,7 @@ export function DashboardScenePage({ match, route, queryParams }: Props) {
|
|||||||
return () => {
|
return () => {
|
||||||
stateManager.clearState();
|
stateManager.clearState();
|
||||||
};
|
};
|
||||||
}, [stateManager, match.params.uid, route.routeName, queryParams.folderUid]);
|
}, [stateManager, match.params.uid, route.routeName, queryParams.folderUid, routeReloadCounter]);
|
||||||
|
|
||||||
if (!dashboard) {
|
if (!dashboard) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||||
|
|
||||||
if (route === DashboardRoutes.Embedded) {
|
if (route === DashboardRoutes.Embedded) {
|
||||||
rsp.meta.isEmbedded = true;
|
rsp.meta.isEmbedded = true;
|
||||||
}
|
}
|
||||||
@@ -188,6 +187,10 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
|||||||
public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) {
|
public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) {
|
||||||
this.dashboardCache = { dashboard, ts: Date.now(), cacheKey };
|
this.dashboardCache = { dashboard, ts: Date.now(), cacheKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearDashboardCache() {
|
||||||
|
this.dashboardCache = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let stateManager: DashboardScenePageStateManager | null = null;
|
let stateManager: DashboardScenePageStateManager | null = null;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
import { VariablesChanged } from 'app/features/variables/types';
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
||||||
|
|
||||||
@@ -136,6 +137,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
this.startTrackingChanges();
|
this.startTrackingChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.meta.isEmbedded && this.state.uid) {
|
||||||
|
dashboardWatcher.watch(this.state.uid);
|
||||||
|
}
|
||||||
|
|
||||||
const clearKeyBindings = setupKeyboardShortcuts(this);
|
const clearKeyBindings = setupKeyboardShortcuts(this);
|
||||||
const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this);
|
const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this);
|
||||||
|
|
||||||
@@ -149,6 +154,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
this.stopTrackingChanges();
|
this.stopTrackingChanges();
|
||||||
this.stopUrlSync();
|
this.stopUrlSync();
|
||||||
oldDashboardWrapper.destroy();
|
oldDashboardWrapper.destroy();
|
||||||
|
dashboardWatcher.leave();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,10 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
this._subs.unsubscribe();
|
this._subs.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasUnsavedChanges() {
|
||||||
|
return this._scene.state.isDirty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PanelCompatibilityWrapper {
|
class PanelCompatibilityWrapper {
|
||||||
|
|||||||
Reference in New Issue
Block a user