Dashboards: Discard the entire panel if it is newly added (#87562)

This commit is contained in:
Ivan Ortega Alba 2024-05-27 13:46:02 +02:00 committed by GitHub
parent c0881cc970
commit 66950c96f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 161 additions and 37 deletions

View File

@ -1491,6 +1491,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"]
],
"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, "Do not use any type assertions.", "0"]
],

View File

@ -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', () => {

View File

@ -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<PanelEditorState> {
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<PanelEditorState> {
};
}
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,
});
}

View File

@ -779,7 +779,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
return getPanelIdForVizPanel(row);
}
public onCreateNewPanel(): number {
public onCreateNewPanel(): VizPanel {
if (!this.state.isEditing) {
this.onEnterEditMode();
}
@ -788,7 +788,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this.addPanel(vizPanel);
return getPanelIdForVizPanel(vizPanel);
return vizPanel;
}
/**

View File

@ -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;
@ -144,6 +145,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
});
return;
}
update.editPanel = buildPanelEditScene(panel);
} else if (editPanel && values.editPanel === null) {
update.editPanel = undefined;

View File

@ -1,4 +1,4 @@
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';
@ -6,11 +6,13 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
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', () => ({
@ -25,6 +27,17 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({
},
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual<Record<string, any>>('@grafana/runtime'),
getDataSourceSrv: () => ({
get: jest.fn(),
getInstanceSettings: jest.fn().mockReturnValue({
uid: 'datasource-uid',
name: 'datasource-name',
}),
}),
}));
describe('NavToolbarActions', () => {
describe('Given an already saved dashboard', () => {
it('Should show correct buttons when not in editing', async () => {
@ -90,8 +103,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'));
@ -101,6 +115,35 @@ 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();
});
});
describe('Given new sharing button', () => {
@ -115,40 +158,51 @@ describe('NavToolbarActions', () => {
config.featureToggles.newDashboardSharingComponent = true;
setup();
expect(screen.queryByTestId(selectors.pages.Dashboard.DashNav.shareButton)).not.toBeInTheDocument();
expect(await screen.queryByTestId(selectors.pages.Dashboard.DashNav.shareButton)).not.toBeInTheDocument();
const newShareButton = screen.getByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container);
expect(newShareButton).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(

View File

@ -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 ShareButton from '../sharing/ShareButton/ShareButton';
import { ShareModal } from '../sharing/ShareModal';
import { DashboardInteractions } from '../utils/interactions';
@ -170,9 +170,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) });
}}
/>
<Menu.Item
@ -415,11 +415,11 @@ export function ToolbarActions({ dashboard }: Props) {
toolbarActions.push({
group: 'main-buttons',
condition: isEditingPanel && !isEditingLibraryPanel && !editview && !meta.isNew && !isViewingPanel,
condition: isEditingPanel && !isEditingLibraryPanel && !editview && !isViewingPanel,
render: () => (
<Button
onClick={editPanel?.onDiscard}
tooltip="Discard panel changes"
tooltip={editPanel?.state.isNewPanel ? 'Discard panel' : 'Discard panel changes'}
size="sm"
disabled={!isEditedPanelDirty || !isDirty}
key="discard"
@ -427,7 +427,7 @@ export function ToolbarActions({ dashboard }: Props) {
variant="destructive"
data-testid={selectors.components.NavToolbar.editDashboard.discardChangesButton}
>
Discard panel changes
{editPanel?.state.isNewPanel ? 'Discard panel' : 'Discard panel changes'}
</Button>
),
});

View File

@ -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();
});

View File

@ -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' });
};