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
parent 368fec9b97
commit fa9e139123
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 31 deletions

View File

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

View File

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

View File

@ -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');
}
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<DashboardSceneState> {
}
public onCreateNewPanel(): number {
if (!this.state.isEditing) {
this.onEnterEditMode();
}
const vizPanel = getDefaultVizPanel(this);
this.addPanel(vizPanel);

View File

@ -62,7 +62,10 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
</div>
)}
<CustomScrollbar autoHeightMin={'100%'} className={styles.scrollbarContainer}>
<div className={styles.canvasContent}>{isEmpty ? emptyState : withPanels}</div>
<div className={styles.canvasContent}>
<>{isEmpty && emptyState}</>
{withPanels}
</div>
</CustomScrollbar>
</div>
)}

View File

@ -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 }),
@ -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[] => {
return snapshotData.map((data) => {
return {
@ -619,3 +604,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();
};
}

View File

@ -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 (
<Stack alignItems="center" justifyContent="center">
<div className={styles.wrapper}>
@ -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}
>
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>