From 99acd3766d60f2cc4d42fba40389b7816b48d494 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:37:42 -0600 Subject: [PATCH] Suggestions: Update empty state (#116172) --- .../components/PanelDataErrorView.test.tsx | 61 ++++++++++++++++++- .../panel/components/PanelDataErrorView.tsx | 32 +++++++++- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/public/app/features/panel/components/PanelDataErrorView.test.tsx b/public/app/features/panel/components/PanelDataErrorView.test.tsx index 7040b80418f..c5c4f96e982 100644 --- a/public/app/features/panel/components/PanelDataErrorView.test.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.test.tsx @@ -2,8 +2,9 @@ import { render, screen } from '@testing-library/react'; import { defaultsDeep } from 'lodash'; import { Provider } from 'react-redux'; -import { FieldType, getDefaultTimeRange, LoadingState } from '@grafana/data'; -import { PanelDataErrorViewProps } from '@grafana/runtime'; +import { CoreApp, EventBusSrv, FieldType, getDefaultTimeRange, LoadingState } from '@grafana/data'; +import { config, PanelDataErrorViewProps } from '@grafana/runtime'; +import { usePanelContext } from '@grafana/ui'; import { configureStore } from 'app/store/configureStore'; import { PanelDataErrorView } from './PanelDataErrorView'; @@ -16,7 +17,24 @@ jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ }, })); +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + usePanelContext: jest.fn(), +})); + +const mockUsePanelContext = jest.mocked(usePanelContext); +const RUN_QUERY_MESSAGE = 'Run a query to visualize it here or go to all visualizations to add other panel types'; +const panelContextRoot = { + app: CoreApp.Dashboard, + eventsScope: 'global', + eventBus: new EventBusSrv(), +}; + describe('PanelDataErrorView', () => { + beforeEach(() => { + mockUsePanelContext.mockReturnValue(panelContextRoot); + }); + it('show No data when there is no data', () => { renderWithProps(); @@ -70,6 +88,45 @@ describe('PanelDataErrorView', () => { expect(screen.getByText('Query returned nothing')).toBeInTheDocument(); }); + + it('should show "Run a query..." message when no query is configured and feature toggle is enabled', () => { + mockUsePanelContext.mockReturnValue(panelContextRoot); + + const originalFeatureToggle = config.featureToggles.newVizSuggestions; + config.featureToggles.newVizSuggestions = true; + + renderWithProps({ + data: { + state: LoadingState.Done, + series: [], + timeRange: getDefaultTimeRange(), + }, + }); + + expect(screen.getByText(RUN_QUERY_MESSAGE)).toBeInTheDocument(); + + config.featureToggles.newVizSuggestions = originalFeatureToggle; + }); + + it('should show "No data" message when feature toggle is disabled even without queries', () => { + mockUsePanelContext.mockReturnValue(panelContextRoot); + + const originalFeatureToggle = config.featureToggles.newVizSuggestions; + config.featureToggles.newVizSuggestions = false; + + renderWithProps({ + data: { + state: LoadingState.Done, + series: [], + timeRange: getDefaultTimeRange(), + }, + }); + + expect(screen.getByText('No data')).toBeInTheDocument(); + expect(screen.queryByText(RUN_QUERY_MESSAGE)).not.toBeInTheDocument(); + + config.featureToggles.newVizSuggestions = originalFeatureToggle; + }); }); function renderWithProps(overrides?: Partial) { diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 93723b3bff1..74c0d494c3f 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -5,14 +5,15 @@ import { FieldType, getPanelDataSummary, GrafanaTheme2, + PanelData, PanelDataSummary, PanelPluginVisualizationSuggestion, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { PanelDataErrorViewProps, locationService } from '@grafana/runtime'; +import { PanelDataErrorViewProps, locationService, config } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; -import { usePanelContext, useStyles2 } from '@grafana/ui'; +import { Icon, usePanelContext, useStyles2 } from '@grafana/ui'; import { CardButton } from 'app/core/components/CardButton'; import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; import store from 'app/core/store'; @@ -24,6 +25,11 @@ import { findVizPanelByKey, getVizPanelKeyForPanelId } from 'app/features/dashbo import { useDispatch } from 'app/types/store'; import { changePanelPlugin } from '../state/actions'; +import { hasData } from '../suggestions/utils'; + +function hasNoQueryConfigured(data: PanelData): boolean { + return !data.request?.targets || data.request.targets.length === 0; +} export function PanelDataErrorView(props: PanelDataErrorViewProps) { const styles = useStyles2(getStyles); @@ -93,8 +99,14 @@ export function PanelDataErrorView(props: PanelDataErrorViewProps) { } }; + const noData = !hasData(props.data); + const noQueryConfigured = hasNoQueryConfigured(props.data); + const showEmptyState = + config.featureToggles.newVizSuggestions && context.app === CoreApp.PanelEditor && noQueryConfigured && noData; + return (
+ {showEmptyState && }
{message}
@@ -131,7 +143,17 @@ function getMessageFor( return message; } - if (!data.series || data.series.length === 0 || data.series.every((frame) => frame.length === 0)) { + const noData = !hasData(data); + const noQueryConfigured = hasNoQueryConfigured(data); + + if (config.featureToggles.newVizSuggestions && noQueryConfigured && noData) { + return t( + 'dashboard.new-panel.empty-state-message', + 'Run a query to visualize it here or go to all visualizations to add other panel types' + ); + } + + if (noData) { return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data'); } @@ -176,5 +198,9 @@ const getStyles = (theme: GrafanaTheme2) => { width: '100%', maxWidth: '600px', }), + emptyStateIcon: css({ + color: theme.colors.text.secondary, + marginBottom: theme.spacing(2), + }), }; };