From cf7ed7441590a0be47b0480a7d4ade92ecced5ed Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:11:18 +0200 Subject: [PATCH] [v11.0.x] DashboardScene: Fix issues with dashboard empty state (#85422) DashboardScene: Fix issues with dashboard empty state (#85406) Fix Tests Make sure edit mode is on when adding panel/library panel Co-authored-by: kay delaney (cherry picked from commit fa9e139123953244ae1430bcdb57efba5f1d103a) Co-authored-by: Dominik Prokop --- .../pages/DashboardScenePage.test.tsx | 43 +++++++++++++++++-- .../scene/DashboardScene.test.tsx | 9 ++++ .../dashboard-scene/scene/DashboardScene.tsx | 8 ++++ .../scene/DashboardSceneRenderer.tsx | 5 ++- .../transformSaveModelToScene.ts | 33 +++++++------- .../dashboard/dashgrid/DashboardEmpty.tsx | 24 +++++++---- 6 files changed, 91 insertions(+), 31 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 3033bd6390c..26b17dcfd41 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -8,10 +8,13 @@ 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 { VizPanel } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; + import { DashboardScenePage, Props } from './DashboardScenePage'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; @@ -195,11 +198,43 @@ describe('DashboardScenePage', () => { expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); }); - it('Shows empty state when dashboard is empty', async () => { - loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); - setup(); + describe('empty state', () => { + it('Shows empty state when dashboard is empty', async () => { + loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); + setup(); - expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument(); + expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument(); + }); + + it('shows and hides empty state when panels are added and removed', async () => { + setup(); + + await waitForDashbordToRender(); + + expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument(); + + // Hacking a bit, accessing private cache property to get access to the underlying DashboardScene object + const dashboardScenesCache = getDashboardScenePageStateManager()['cache']; + const dashboard = dashboardScenesCache['my-dash-uid']; + const panels = dashboardSceneGraph.getVizPanels(dashboard); + + act(() => { + dashboard.removePanel(panels[0]); + }); + expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument(); + + act(() => { + dashboard.removePanel(panels[1]); + }); + expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument(); + + act(() => { + dashboard.addPanel(new VizPanel({ title: 'Panel Added', key: 'panel-4', pluginId: 'timeseries' })); + }); + + expect(await screen.findByTitle('Panel Added')).toBeInTheDocument(); + expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index fb8bb08a8bf..25bd116200e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -283,11 +283,15 @@ describe('DashboardScene', () => { }); it('Should create and add a new panel to the dashboard', () => { + scene.exitEditMode({ skipConfirm: true }); + expect(scene.state.isEditing).toBe(false); + scene.onCreateNewPanel(); const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as DashboardGridItem; + expect(scene.state.isEditing).toBe(true); expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-7'); }); @@ -298,6 +302,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridRow = body.state.children[0] as SceneGridRow; + expect(scene.state.isEditing).toBe(true); expect(body.state.children.length).toBe(4); expect(gridRow.state.key).toBe('panel-7'); expect(gridRow.state.children[0].state.key).toBe('griditem-1'); @@ -509,11 +514,15 @@ describe('DashboardScene', () => { }); it('Should create a new add library panel widget', () => { + scene.exitEditMode({ skipConfirm: true }); + expect(scene.state.isEditing).toBe(false); + scene.onCreateLibPanelWidget(); const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as DashboardGridItem; + expect(scene.state.isEditing).toBe(true); expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(gridItem.state.y).toBe(0); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index f9f90ebf0a4..cbac30ea17c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -740,6 +740,10 @@ export class DashboardScene extends SceneObjectBase { throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); } + if (!this.state.isEditing) { + this.onEnterEditMode(); + } + const sceneGridLayout = this.state.body; const panelId = dashboardSceneGraph.getNextPanelId(this); @@ -767,6 +771,10 @@ export class DashboardScene extends SceneObjectBase { } public onCreateNewPanel(): number { + if (!this.state.isEditing) { + this.onEnterEditMode(); + } + const vizPanel = getDefaultVizPanel(this); this.addPanel(vizPanel); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 0d346eafa12..7a0ab70610e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -62,7 +62,10 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps )} -
{isEmpty ? emptyState : withPanels}
+
+ <>{isEmpty && emptyState} + {withPanels} +
)} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b92c7a847dc..d28167176c1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -26,7 +26,6 @@ import { UserActionEvent, GroupByVariable, AdHocFiltersVariable, - SceneFlexLayout, } from '@grafana/scenes'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; @@ -52,6 +51,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { DashboardInteractions } from '../utils/interactions'; import { getCurrentValueForOldIntervalModel, + getDashboardSceneFor, getIntervalsFromQueryString, getVizPanelKeyForPanelId, } from '../utils/utils'; @@ -285,6 +285,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) body: new SceneGridLayout({ isLazy: true, children: createSceneObjectsForPanels(oldModel.panels), + $behaviors: [trackIfEmpty], }), $timeRange: new SceneTimeRange({ from: oldModel.time.from, @@ -303,7 +304,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) registerDashboardMacro, registerDashboardSceneTracking(oldModel), registerPanelInteractionsReporter, - trackIfIsEmpty, new behaviors.LiveNowTimer(oldModel.liveNow), ], $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), @@ -587,21 +587,6 @@ function registerPanelInteractionsReporter(scene: DashboardScene) { }); } -export function trackIfIsEmpty(parent: DashboardScene) { - updateIsEmpty(parent); - - parent.state.body.subscribeToState(() => { - updateIsEmpty(parent); - }); -} - -function updateIsEmpty(parent: DashboardScene) { - const { body } = parent.state; - if (body instanceof SceneFlexLayout || body instanceof SceneGridLayout) { - parent.setState({ isEmpty: body.state.children.length === 0 }); - } -} - const convertSnapshotData = (snapshotData: DataFrameDTO[]): DataFrameJSON[] => { return snapshotData.map((data) => { return { @@ -636,3 +621,17 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => { panel.snapshotData = []; } }; + +function trackIfEmpty(grid: SceneGridLayout) { + getDashboardSceneFor(grid).setState({ isEmpty: grid.state.children.length === 0 }); + + const sub = grid.subscribeToState((n, p) => { + if (n.children.length !== p.children.length || n.children !== p.children) { + getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 }); + } + }); + + return () => { + sub.unsubscribe(); + }; +} diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 9de79fbc1de..3602a5f188d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -7,7 +7,11 @@ import { config, locationService } from '@grafana/runtime'; import { Button, useStyles2, Text, Box, Stack } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { DashboardModel } from 'app/features/dashboard/state'; -import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard'; +import { + onAddLibraryPanel as onAddLibraryPanelImpl, + onCreateNewPanel, + onImportDashboard, +} from 'app/features/dashboard/utils/dashboard'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { useDispatch, useSelector } from 'app/types'; @@ -37,6 +41,15 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_visualization' }); }; + const onAddLibraryPanel = () => { + DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' }); + if (dashboard instanceof DashboardScene) { + dashboard.onCreateLibPanelWidget(); + } else { + onAddLibraryPanelImpl(dashboard); + } + }; + return (
@@ -110,14 +123,7 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { icon="plus" fill="outline" data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')} - onClick={() => { - DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' }); - if (dashboard instanceof DashboardScene) { - dashboard.onCreateLibPanelWidget(); - } else { - onAddLibraryPanel(dashboard); - } - }} + onClick={onAddLibraryPanel} disabled={!canCreate} > Add library panel