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 <kay@grafana.com>
This commit is contained in:
Dominik Prokop
2024-03-30 18:34:26 +01:00
committed by GitHub
co-authored by kay delaney
parent 368fec9b97
commit fa9e139123
6 changed files with 91 additions and 31 deletions
@@ -8,10 +8,13 @@ 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 { VizPanel } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema'; 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 { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { DashboardScenePage, Props } from './DashboardScenePage'; import { DashboardScenePage, Props } from './DashboardScenePage';
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
@@ -195,12 +198,44 @@ describe('DashboardScenePage', () => {
expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
}); });
describe('empty state', () => {
it('Shows empty state when dashboard is empty', async () => { it('Shows empty state when dashboard is empty', async () => {
loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} });
setup(); 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();
});
});
}); });
interface VizOptions { interface VizOptions {
@@ -283,11 +283,15 @@ describe('DashboardScene', () => {
}); });
it('Should create and add a new panel to the dashboard', () => { it('Should create and add a new panel to the dashboard', () => {
scene.exitEditMode({ skipConfirm: true });
expect(scene.state.isEditing).toBe(false);
scene.onCreateNewPanel(); scene.onCreateNewPanel();
const body = scene.state.body as SceneGridLayout; const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[0] as DashboardGridItem; const gridItem = body.state.children[0] as DashboardGridItem;
expect(scene.state.isEditing).toBe(true);
expect(body.state.children.length).toBe(6); expect(body.state.children.length).toBe(6);
expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(gridItem.state.body!.state.key).toBe('panel-7');
}); });
@@ -298,6 +302,7 @@ describe('DashboardScene', () => {
const body = scene.state.body as SceneGridLayout; const body = scene.state.body as SceneGridLayout;
const gridRow = body.state.children[0] as SceneGridRow; const gridRow = body.state.children[0] as SceneGridRow;
expect(scene.state.isEditing).toBe(true);
expect(body.state.children.length).toBe(4); expect(body.state.children.length).toBe(4);
expect(gridRow.state.key).toBe('panel-7'); expect(gridRow.state.key).toBe('panel-7');
expect(gridRow.state.children[0].state.key).toBe('griditem-1'); 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', () => { it('Should create a new add library panel widget', () => {
scene.exitEditMode({ skipConfirm: true });
expect(scene.state.isEditing).toBe(false);
scene.onCreateLibPanelWidget(); scene.onCreateLibPanelWidget();
const body = scene.state.body as SceneGridLayout; const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[0] as DashboardGridItem; const gridItem = body.state.children[0] as DashboardGridItem;
expect(scene.state.isEditing).toBe(true);
expect(body.state.children.length).toBe(6); expect(body.state.children.length).toBe(6);
expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(gridItem.state.body!.state.key).toBe('panel-7');
expect(gridItem.state.y).toBe(0); expect(gridItem.state.y).toBe(0);
@@ -740,6 +740,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); 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 sceneGridLayout = this.state.body;
const panelId = dashboardSceneGraph.getNextPanelId(this); const panelId = dashboardSceneGraph.getNextPanelId(this);
@@ -767,6 +771,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
public onCreateNewPanel(): number { public onCreateNewPanel(): number {
if (!this.state.isEditing) {
this.onEnterEditMode();
}
const vizPanel = getDefaultVizPanel(this); const vizPanel = getDefaultVizPanel(this);
this.addPanel(vizPanel); this.addPanel(vizPanel);
@@ -62,7 +62,10 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
</div> </div>
)} )}
<CustomScrollbar autoHeightMin={'100%'} className={styles.scrollbarContainer}> <CustomScrollbar autoHeightMin={'100%'} className={styles.scrollbarContainer}>
<div className={styles.canvasContent}>{isEmpty ? emptyState : withPanels}</div> <div className={styles.canvasContent}>
<>{isEmpty && emptyState}</>
{withPanels}
</div>
</CustomScrollbar> </CustomScrollbar>
</div> </div>
)} )}
@@ -26,7 +26,6 @@ import {
UserActionEvent, UserActionEvent,
GroupByVariable, GroupByVariable,
AdHocFiltersVariable, AdHocFiltersVariable,
SceneFlexLayout,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking';
@@ -52,6 +51,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { import {
getCurrentValueForOldIntervalModel, getCurrentValueForOldIntervalModel,
getDashboardSceneFor,
getIntervalsFromQueryString, getIntervalsFromQueryString,
getVizPanelKeyForPanelId, getVizPanelKeyForPanelId,
} from '../utils/utils'; } from '../utils/utils';
@@ -285,6 +285,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
body: new SceneGridLayout({ body: new SceneGridLayout({
isLazy: true, isLazy: true,
children: createSceneObjectsForPanels(oldModel.panels), children: createSceneObjectsForPanels(oldModel.panels),
$behaviors: [trackIfEmpty],
}), }),
$timeRange: new SceneTimeRange({ $timeRange: new SceneTimeRange({
from: oldModel.time.from, from: oldModel.time.from,
@@ -303,7 +304,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
registerDashboardMacro, registerDashboardMacro,
registerDashboardSceneTracking(oldModel), registerDashboardSceneTracking(oldModel),
registerPanelInteractionsReporter, registerPanelInteractionsReporter,
trackIfIsEmpty,
new behaviors.LiveNowTimer(oldModel.liveNow), new behaviors.LiveNowTimer(oldModel.liveNow),
], ],
$data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
@@ -570,21 +570,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[] => { const convertSnapshotData = (snapshotData: DataFrameDTO[]): DataFrameJSON[] => {
return snapshotData.map((data) => { return snapshotData.map((data) => {
return { return {
@@ -619,3 +604,17 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => {
panel.snapshotData = []; 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();
};
}
@@ -7,7 +7,11 @@ import { config, locationService } from '@grafana/runtime';
import { Button, useStyles2, Text, Box, Stack } from '@grafana/ui'; import { Button, useStyles2, Text, Box, Stack } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans } from 'app/core/internationalization';
import { DashboardModel } from 'app/features/dashboard/state'; 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 { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { useDispatch, useSelector } from 'app/types'; import { useDispatch, useSelector } from 'app/types';
@@ -37,6 +41,15 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_visualization' }); DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_visualization' });
}; };
const onAddLibraryPanel = () => {
DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' });
if (dashboard instanceof DashboardScene) {
dashboard.onCreateLibPanelWidget();
} else {
onAddLibraryPanelImpl(dashboard);
}
};
return ( return (
<Stack alignItems="center" justifyContent="center"> <Stack alignItems="center" justifyContent="center">
<div className={styles.wrapper}> <div className={styles.wrapper}>
@@ -110,14 +123,7 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
icon="plus" icon="plus"
fill="outline" fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')} data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
onClick={() => { onClick={onAddLibraryPanel}
DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' });
if (dashboard instanceof DashboardScene) {
dashboard.onCreateLibPanelWidget();
} else {
onAddLibraryPanel(dashboard);
}
}}
disabled={!canCreate} disabled={!canCreate}
> >
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans> <Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>