diff --git a/.betterer.results b/.betterer.results index 983e03cb8ac..f84227274d1 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2417,6 +2417,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] ], + "public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index af90d25282d..abca0835fc4 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -59,6 +59,63 @@ describe('PanelEditor', () => { const updatedPanel = gridItem.state.body as VizPanel; expect(updatedPanel?.state.title).toBe('changed title'); }); + + it('should discard changes when unmounted and discard changes is marked as true', () => { + pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true }); + + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const gridItem = new DashboardGridItem({ body: panel }); + + const editScene = buildPanelEditScene(panel); + const scene = new DashboardScene({ + editPanel: editScene, + isEditing: true, + body: new SceneGridLayout({ + children: [gridItem], + }), + }); + + const deactivate = activateFullSceneTree(scene); + + editScene.state.vizManager.state.panel.setState({ title: 'changed title' }); + + editScene.onDiscard(); + deactivate(); + + const updatedPanel = gridItem.state.body as VizPanel; + expect(updatedPanel?.state.title).toBe(panel.state.title); + }); + + it('should discard a newly added panel', () => { + pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true }); + + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const gridItem = new DashboardGridItem({ body: panel }); + + const editScene = buildPanelEditScene(panel, true); + const scene = new DashboardScene({ + editPanel: editScene, + isEditing: true, + body: new SceneGridLayout({ + children: [gridItem], + }), + }); + + editScene.onDiscard(); + const deactivate = activateFullSceneTree(scene); + + deactivate(); + + expect((scene.state.body as SceneGridLayout).state.children.length).toBe(0); + }); }); describe('Handling library panels', () => { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 4e04a233ec0..ade8215e20c 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -14,6 +14,7 @@ import { PanelOptionsPane } from './PanelOptionsPane'; import { VizPanelManager, VizPanelManagerState } from './VizPanelManager'; export interface PanelEditorState extends SceneObjectState { + isNewPanel: boolean; isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; @@ -59,6 +60,8 @@ export class PanelEditor extends SceneObjectBase { return () => { if (!this._discardChanges) { this.commitChanges(); + } else if (this.state.isNewPanel) { + getDashboardSceneFor(this).removePanel(panelManager.state.sourcePanel.resolve()!); } }; } @@ -173,10 +176,11 @@ export class PanelEditor extends SceneObjectBase { }; } -export function buildPanelEditScene(panel: VizPanel): PanelEditor { +export function buildPanelEditScene(panel: VizPanel, isNewPanel = false): PanelEditor { return new PanelEditor({ panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), vizManager: VizPanelManager.createFor(panel), + isNewPanel, }); } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 1b0e41dca2c..8a7b37a8005 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -778,7 +778,7 @@ export class DashboardScene extends SceneObjectBase { return getPanelIdForVizPanel(row); } - public onCreateNewPanel(): number { + public onCreateNewPanel(): VizPanel { if (!this.state.isEditing) { this.onEnterEditMode(); } @@ -787,7 +787,7 @@ export class DashboardScene extends SceneObjectBase { this.addPanel(vizPanel); - return getPanelIdForVizPanel(vizPanel); + return vizPanel; } /** diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 90a8a932491..b4ac8114a2e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -124,6 +124,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { // Handle edit panel state if (typeof values.editPanel === 'string') { const panel = findVizPanelByKey(this._scene, values.editPanel); + if (!panel) { console.warn(`Panel ${values.editPanel} not found`); return; @@ -139,6 +140,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { }); return; } + update.editPanel = buildPanelEditScene(panel); } else if (editPanel && values.editPanel === null) { update.editPanel = undefined; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 7037971a2e6..e88658d70ea 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -1,15 +1,17 @@ -import { screen, render } from '@testing-library/react'; +import { screen, render, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { selectors } from '@grafana/e2e-selectors'; +import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; -import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; +import { buildPanelEditScene } from '../panel-edit/PanelEditor'; +import { DashboardGridItem } from './DashboardGridItem'; +import { DashboardScene } from './DashboardScene'; import { ToolbarActions } from './NavToolbarActions'; jest.mock('app/features/playlist/PlaylistSrv', () => ({ @@ -24,6 +26,17 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({ }, })); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual>('@grafana/runtime'), + getDataSourceSrv: () => ({ + get: jest.fn(), + getInstanceSettings: jest.fn().mockReturnValue({ + uid: 'datasource-uid', + name: 'datasource-name', + }), + }), +})); + describe('NavToolbarActions', () => { describe('Give an already saved dashboard', () => { it('Should show correct buttons when not in editing', async () => { @@ -89,8 +102,9 @@ describe('NavToolbarActions', () => { }); it('Should show correct buttons when in settings menu', async () => { - setup(); + const { dashboard } = setup(); + dashboard.startUrlSync(); await userEvent.click(await screen.findByText('Edit')); await userEvent.click(await screen.findByText('Settings')); @@ -100,36 +114,76 @@ describe('NavToolbarActions', () => { expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); }); + + it('Should show correct buttons when editing a new panel', async () => { + const { dashboard } = setup(); + await act(() => { + dashboard.onEnterEditMode(); + const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state + .body as VizPanel; + dashboard.setState({ editPanel: buildPanelEditScene(editingPanel, true) }); + }); + + expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); + expect(await screen.findByText('Discard panel')).toBeInTheDocument(); + expect(await screen.findByText('Back to dashboard')).toBeInTheDocument(); + }); + + it('Should show correct buttons when editing an existing panel', async () => { + const { dashboard } = setup(); + + await act(() => { + dashboard.onEnterEditMode(); + const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state + .body as VizPanel; + dashboard.setState({ editPanel: buildPanelEditScene(editingPanel) }); + }); + + expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); + expect(await screen.findByText('Discard panel changes')).toBeInTheDocument(); + expect(await screen.findByText('Back to dashboard')).toBeInTheDocument(); + }); }); }); -let cleanUp = () => {}; - function setup() { - const dashboard = transformSaveModelToScene({ - dashboard: { - title: 'hello', - uid: 'my-uid', - schemaVersion: 30, - panels: [], - version: 10, - }, + const dashboard = new DashboardScene({ + $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), meta: { + canEdit: true, + isNew: false, + canMakeEditable: true, canSave: true, + canShare: true, + canStar: true, + canAdmin: true, + canDelete: true, }, + title: 'hello', + uid: 'dash-1', + body: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }), }); - // Clear any data layers - dashboard.setState({ $data: undefined }); - - const initialSaveModel = transformSceneToSaveModel(dashboard); - dashboard.setInitialSaveModel(initialSaveModel); - - dashboard.startUrlSync(); - - cleanUp(); - cleanUp = dashboard.activate(); - const context = getGrafanaContextMock(); render( diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 957f78d4724..0df57cbaf24 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -22,7 +22,7 @@ import { Trans, t } from 'app/core/internationalization'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { PanelEditor } from '../panel-edit/PanelEditor'; +import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor'; import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; @@ -169,9 +169,9 @@ export function ToolbarActions({ dashboard }: Props) { testId={selectors.pages.AddDashboard.itemButton('Add new visualization menu item')} label={t('dashboard.add-menu.visualization', 'Visualization')} onClick={() => { - const id = dashboard.onCreateNewPanel(); + const vizPanel = dashboard.onCreateNewPanel(); DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' }); - locationService.partial({ editPanel: id }); + dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) }); }} /> ( ), }); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx index bef047baa75..8f47962a4d0 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx @@ -76,6 +76,7 @@ it('creates new visualization when clicked Add visualization', () => { expect(reportInteraction).toHaveBeenCalledWith('dashboards_emptydashboard_clicked', { item: 'add_visualization' }); expect(locationService.partial).toHaveBeenCalled(); + expect(locationService.partial).toHaveBeenCalledWith({ editPanel: undefined, firstPanel: true }); expect(onCreateNewPanel).toHaveBeenCalled(); }); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index b2fdb9f6267..e1f72b1eb6a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -12,6 +12,7 @@ import { onCreateNewPanel, onImportDashboard, } from 'app/features/dashboard/utils/dashboard'; +import { buildPanelEditScene } from 'app/features/dashboard-scene/panel-edit/PanelEditor'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { useDispatch, useSelector } from 'app/types'; @@ -31,13 +32,15 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { const onAddVisualization = () => { let id; if (dashboard instanceof DashboardScene) { - id = dashboard.onCreateNewPanel(); + const panel = dashboard.onCreateNewPanel(); + dashboard.setState({ editPanel: buildPanelEditScene(panel, true) }); + locationService.partial({ firstPanel: true }); } else { id = onCreateNewPanel(dashboard, initialDatasource); dispatch(setInitialDatasource(undefined)); + locationService.partial({ editPanel: id, firstPanel: true }); } - locationService.partial({ editPanel: id, firstPanel: true }); DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_visualization' }); };