diff --git a/.betterer.results b/.betterer.results index 484b913ab67..fcaefeed57e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2379,6 +2379,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [ [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"] + ], "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2753,6 +2756,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"] ], + "public/app/features/dashboard/containers/DashboardPageProxy.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -2909,9 +2915,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], - "public/app/features/dashboard/state/initDashboard.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/dashboard/utils/getPanelMenu.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx index 7cab41afc7c..a13376d3116 100644 --- a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx +++ b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2, urlUtil } from '@grafana/data'; import { EmbeddedDashboardProps } from '@grafana/runtime'; import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes'; import { Spinner, Alert, useStyles2 } from '@grafana/ui'; +import { DashboardRoutes } from 'app/types'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; @@ -14,7 +15,7 @@ export function EmbeddedDashboard(props: EmbeddedDashboardProps) { const { dashboard, loadError } = stateManager.useState(); useEffect(() => { - stateManager.loadDashboard({ uid: props.uid!, isEmbedded: true }); + stateManager.loadDashboard({ uid: props.uid!, route: DashboardRoutes.Embedded }); return () => { stateManager.clearState(); }; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 67cd440c42b..19251e72e38 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -5,28 +5,28 @@ import { PageLayoutType } from '@grafana/data'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { DashboardPageRouteParams } from 'app/features/dashboard/containers/types'; +import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; import { DashboardRoutes } from 'app/types'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; -export interface Props extends GrafanaRouteComponentProps {} +export interface Props extends GrafanaRouteComponentProps {} -export function DashboardScenePage({ match, route }: Props) { +export function DashboardScenePage({ match, route, queryParams }: Props) { const stateManager = getDashboardScenePageStateManager(); const { dashboard, isLoading, loadError } = stateManager.useState(); useEffect(() => { - if (route.routeName === DashboardRoutes.Home) { - stateManager.loadDashboard({ uid: route.routeName }); - } else { - stateManager.loadDashboard({ uid: match.params.uid! }); - } + stateManager.loadDashboard({ + uid: match.params.uid ?? '', + route: route.routeName as DashboardRoutes, + urlFolderUid: queryParams.folderUid, + }); return () => { stateManager.clearState(); }; - }, [stateManager, match.params.uid, route.routeName]); + }, [stateManager, match.params.uid, route.routeName, queryParams.folderUid]); if (!dashboard) { return ( diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index df2b87c9279..7c80bc740df 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -2,6 +2,7 @@ import { advanceBy } from 'jest-date-mock'; import { locationService } from '@grafana/runtime'; import { getUrlSyncManager } from '@grafana/scenes'; +import { DashboardRoutes } from 'app/types'; import { DashboardScene } from '../scene/DashboardScene'; import { setupLoadDashboardMock } from '../utils/test-utils'; @@ -14,12 +15,12 @@ describe('DashboardScenePageStateManager', () => { const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', editable: true }, meta: {} }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash'); // should use cache second time - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashboardMock.mock.calls.length).toBe(1); }); @@ -27,7 +28,7 @@ describe('DashboardScenePageStateManager', () => { setupLoadDashboardMock({ dashboard: undefined, meta: {} }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loader.state.dashboard).toBeUndefined(); expect(loader.state.isLoading).toBe(false); @@ -38,7 +39,7 @@ describe('DashboardScenePageStateManager', () => { setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loader.state.dashboard?.state.uid).toBe('fake-dash'); expect(loader.state.loadError).toBe(undefined); @@ -49,7 +50,7 @@ describe('DashboardScenePageStateManager', () => { setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loader.state.dashboard).toBeInstanceOf(DashboardScene); expect(loader.state.isLoading).toBe(false); @@ -61,7 +62,7 @@ describe('DashboardScenePageStateManager', () => { locationService.partial({ from: 'now-5m', to: 'now' }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); const dash = loader.state.dashboard; expect(dash!.state.$timeRange?.state.from).toEqual('now-5m'); @@ -71,7 +72,7 @@ describe('DashboardScenePageStateManager', () => { // try loading again (and hitting cache) locationService.partial({ from: 'now-10m', to: 'now' }); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); const dash2 = loader.state.dashboard; expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m'); @@ -83,12 +84,32 @@ describe('DashboardScenePageStateManager', () => { locationService.partial({ from: 'now-5m', to: 'now' }); const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash', isEmbedded: true }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Embedded }); const dash = loader.state.dashboard; expect(dash!.state.$timeRange?.state.from).toEqual('now-6h'); }); + describe('New dashboards', () => { + it('Should have new empty model with meta.isNew and should not be cached', async () => { + const loader = new DashboardScenePageStateManager({}); + + await loader.loadDashboard({ uid: '', route: DashboardRoutes.New }); + const dashboard = loader.state.dashboard!; + + expect(dashboard.state.meta.isNew).toBe(true); + expect(dashboard.state.isEditing).toBe(true); + expect(dashboard.state.isDirty).toBe(true); + + dashboard.setState({ title: 'Changed' }); + + await loader.loadDashboard({ uid: '', route: DashboardRoutes.New }); + const dashboard2 = loader.state.dashboard!; + + expect(dashboard2.state.title).toBe('New dashboard'); + }); + }); + describe('caching', () => { it('should cache the dashboard DTO', async () => { setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); @@ -97,7 +118,7 @@ describe('DashboardScenePageStateManager', () => { expect(loader.getFromCache('fake-dash')).toBeNull(); - await loader.loadDashboard({ uid: 'fake-dash' }); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loader.getFromCache('fake-dash')).toBeDefined(); }); @@ -110,15 +131,15 @@ describe('DashboardScenePageStateManager', () => { expect(loader.getFromCache('fake-dash')).toBeNull(); - await loader.fetchDashboard({ uid: 'fake-dash' }); + await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashSpy).toHaveBeenCalledTimes(1); advanceBy(DASHBOARD_CACHE_TTL / 2); - await loader.fetchDashboard({ uid: 'fake-dash' }); + await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashSpy).toHaveBeenCalledTimes(1); advanceBy(DASHBOARD_CACHE_TTL / 2 + 1); - await loader.fetchDashboard({ uid: 'fake-dash' }); + await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashSpy).toHaveBeenCalledTimes(2); }); }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index f061185d88a..c65ccb124dc 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -11,6 +11,7 @@ import { DashboardDTO, DashboardRoutes } from 'app/types'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; +import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; export interface DashboardScenePageState { @@ -22,6 +23,9 @@ export interface DashboardScenePageState { export const DASHBOARD_CACHE_TTL = 2000; +/** Only used by cache in loading home in DashboardPageProxy and initDashboard (Old arch), can remove this after old dashboard arch is gone */ +export const HOME_DASHBOARD_CACHE_KEY = '__grafana_home_uid__'; + interface DashboardCacheEntry { dashboard: DashboardDTO; ts: number; @@ -29,7 +33,8 @@ interface DashboardCacheEntry { export interface LoadDashboardOptions { uid: string; - isEmbedded?: boolean; + route: DashboardRoutes; + urlFolderUid?: string; } export class DashboardScenePageStateManager extends StateManagerBase { @@ -39,8 +44,9 @@ export class DashboardScenePageStateManager extends StateManagerBase { const fromCache = this.cache[options.uid]; if (fromCache) { - // Need to update this in case we cached an embedded but now opening it standard mode - fromCache.state.meta.isEmbedded = options.isEmbedded; return fromCache; } @@ -127,13 +143,12 @@ export class DashboardScenePageStateManager extends StateManagerBase { } public startUrlSync() { - getUrlSyncManager().initSync(this); + if (!this.state.meta.isEmbedded) { + getUrlSyncManager().initSync(this); + } } public stopUrlSync() { @@ -206,6 +208,11 @@ export class DashboardScene extends SceneObjectBase { } public onDiscard = () => { + if (!this.canDiscard()) { + console.error('Trying to discard back to a state that does not exist, initialState undefined'); + return; + } + // No need to listen to changes anymore this.stopTrackingChanges(); // Stop url sync before updating url @@ -233,6 +240,10 @@ export class DashboardScene extends SceneObjectBase { this.propagateEditModeChange(); }; + public canDiscard() { + return this._initialState !== undefined; + } + public onRestore = async (version: DecoratedRevisionModel): Promise => { const versionRsp = await historySrv.restoreDashboard(version.uid, version.version); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 957ee3b9860..0f3bbfdfb0c 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -128,33 +128,37 @@ export const NavToolbarActions = React.memo(({ dashboard }) => { } } else { if (dashboard.canEditDashboard()) { - toolbarActions.push( - - ); - toolbarActions.push( - - ); + if (!dashboard.state.meta.isNew) { + toolbarActions.push( + + ); + } + if (dashboard.canDiscard()) { + toolbarActions.push( + + ); + } toolbarActions.push(