VizSuggestions: Empty state (#114049)

* newVizSuggestions feature toggle

* panel empty state

* add data check

* select first suggestion

* update text

* PanelEmptyState component

* add test

* ?

* remove fake translation

* updates

* move empty state to UnconfiguredPanel
This commit is contained in:
Adela Almasan
2025-11-21 13:42:41 -05:00
committed by GitHub
parent 0e460a267e
commit 8cf777b5e8
5 changed files with 144 additions and 13 deletions
@@ -13,6 +13,7 @@ import {
SceneObjectState,
SceneObjectStateChangedEvent,
SceneQueryRunner,
sceneGraph,
sceneUtils,
VizPanel,
isSceneObject,
@@ -21,6 +22,7 @@ import { Panel } from '@grafana/schema';
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { saveLibPanel } from 'app/features/library-panels/state/api';
import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions';
import { DashboardEditActionEvent } from '../edit-pane/shared';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
@@ -29,6 +31,7 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
import {
activateSceneObjectAndParentTree,
getDashboardSceneFor,
@@ -84,7 +87,11 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
const panel = this.state.panelRef.resolve();
if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
panel.changePluginType('timeseries');
if (config.featureToggles.newVizSuggestions) {
this._autoSelectVisualization(panel);
} else {
panel.changePluginType('timeseries');
}
}
this._subs.add(
@@ -117,6 +124,31 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
};
}
private async _autoSelectVisualization(panel: VizPanel) {
const dataObject = sceneGraph.getData(panel);
this._subs.add(
dataObject.subscribeToState(async () => {
const { data } = dataObject.state;
const hasData = data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0);
if (hasData && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
const panelModel = new PanelModelCompatibilityWrapper(panel);
const suggestions = await getAllSuggestions(data, panelModel);
if (suggestions.length > 0) {
const defaultFirstSuggestion = suggestions[0];
await panel.changePluginType(
defaultFirstSuggestion.pluginId,
defaultFirstSuggestion.options,
defaultFirstSuggestion.fieldConfig
);
}
}
})
);
}
private commitChanges() {
if (!this.state.isDirty && !this._changesHaveBeenMade) {
// Nothing to commit
@@ -228,12 +260,17 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
})
);
const isUnconfigured = Boolean(
config.featureToggles.newVizSuggestions && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID
);
// Setup options pane
this.setState({
optionsPane: new PanelOptionsPane({
panelRef: this.state.panelRef,
searchQuery: '',
listMode: OptionFilter.All,
isVizPickerOpen: isUnconfigured,
}),
isInitializing: false,
});
@@ -1,11 +1,13 @@
import { useCallback, useState } from 'react';
import { css } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import { PanelPlugin, PanelProps } from '@grafana/data';
import { CoreApp, GrafanaTheme2, PanelPlugin, PanelProps } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { locationService } from '@grafana/runtime';
import { config, locationService } from '@grafana/runtime';
import { sceneUtils } from '@grafana/scenes';
import { Box, Button, ButtonGroup, Dropdown, Menu, Stack } from '@grafana/ui';
import { Box, Button, ButtonGroup, Dropdown, Icon, Menu, Stack, Text, usePanelContext, useStyles2 } from '@grafana/ui';
import { NEW_PANEL_TITLE } from '../../dashboard/utils/dashboard';
import { DashboardInteractions } from '../utils/interactions';
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
@@ -16,6 +18,8 @@ const UnconfiguredPanel = new PanelPlugin(UnconfiguredPanelComp);
function UnconfiguredPanelComp(props: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
const panelContext = usePanelContext();
const styles = useStyles2(getStyles);
const onMenuClick = useCallback((isOpen: boolean) => {
setIsOpen(isOpen);
@@ -26,14 +30,15 @@ function UnconfiguredPanelComp(props: PanelProps) {
DashboardInteractions.panelActionClicked('configure', props.id, 'panel');
};
const onUseLibraryPanel = () => {
const dashboard = window.__grafanaSceneContext;
const dashboard = window.__grafanaSceneContext;
const panel =
dashboard instanceof DashboardScene ? findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id)) : null;
if (!(dashboard instanceof DashboardScene)) {
const onUseLibraryPanel = () => {
if (!dashboard || !(dashboard instanceof DashboardScene)) {
throw new Error('DashboardScene not found');
}
const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id));
if (!panel) {
throw new Error('Panel not found');
}
@@ -41,6 +46,18 @@ function UnconfiguredPanelComp(props: PanelProps) {
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
};
useEffect(() => {
if (!panel || !config.featureToggles.newVizSuggestions) {
return;
}
if (panelContext.app === CoreApp.PanelEditor) {
panel.setState({ title: '' });
} else if (!panel.state.title) {
panel.setState({ title: NEW_PANEL_TITLE });
}
}, [panel, panelContext.app]);
const MenuActions = () => (
<Menu>
<Menu.Item
@@ -56,6 +73,25 @@ function UnconfiguredPanelComp(props: PanelProps) {
</Menu>
);
const showEmptyState = config.featureToggles.newVizSuggestions && panelContext.app === CoreApp.PanelEditor;
if (showEmptyState) {
const defaultContent = (
<Trans i18nKey="dashboard.new-panel.empty-state-message">
Run a query to visualize it here or go to all visualizations to add other panel types
</Trans>
);
return (
<div className={styles.emptyStateWrapper}>
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
<Text element="p" textAlignment="center" color="secondary">
{defaultContent}
</Text>
</div>
);
}
return (
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
<Box paddingBottom={2}>
@@ -79,3 +115,20 @@ sceneUtils.registerRuntimePanelPlugin({
pluginId: UNCONFIGURED_PANEL_PLUGIN_ID,
plugin: UnconfiguredPanel,
});
function getStyles(theme: GrafanaTheme2) {
return {
emptyStateWrapper: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center',
}),
emptyStateIcon: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
};
}
@@ -96,6 +96,7 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP
return null;
}
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
if (!key) {
return null;
@@ -258,8 +259,15 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null {
}
export function getDefaultVizPanel(): VizPanel {
const defaultPluginId = config.featureToggles.dashboardNewLayouts ? UNCONFIGURED_PANEL_PLUGIN_ID : 'timeseries';
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
const defaultPluginId =
config.featureToggles.dashboardNewLayouts || config.featureToggles.newVizSuggestions
? UNCONFIGURED_PANEL_PLUGIN_ID
: 'timeseries';
const newPanelTitle =
config.featureToggles.newVizSuggestions && defaultPluginId === UNCONFIGURED_PANEL_PLUGIN_ID
? ''
: t('dashboard.new-panel-title', 'New panel');
return new VizPanel({
title: newPanelTitle,
@@ -5,7 +5,8 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { config } from '@grafana/runtime';
import { Icon, Text, useStyles2 } from '@grafana/ui';
import { getAllSuggestions } from '../../suggestions/getAllSuggestions';
@@ -31,6 +32,21 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel, t
return result;
}, [searchQuery, suggestions, trackSearch]);
const hasData = data?.series && data.series.length > 0 && !data.series.every((frame) => frame.length === 0);
if (config.featureToggles.newVizSuggestions && !hasData && !searchQuery) {
return (
<div className={styles.emptyStateWrapper}>
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.new-panel.suggestions.empty-state-message">
Run a query to start seeing suggested visualizations
</Trans>
</Text>
</div>
);
}
return (
// This div is needed in some places to make AutoSizer work
<div>
@@ -116,5 +132,18 @@ const getStyles = (theme: GrafanaTheme2) => {
marginBottom: theme.spacing(1),
justifyContent: 'space-evenly',
}),
emptyStateWrapper: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(4),
textAlign: 'center',
minHeight: '200px',
}),
emptyStateIcon: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
};
};
+5 -1
View File
@@ -5100,8 +5100,12 @@
"new-panel": {
"configure-button": "Configure",
"configure-button-menu": "Toggle menu",
"empty-state-message": "Run a query to visualize it here or go to all visualizations to add other panel types",
"menu-open-panel-editor": "Configure",
"menu-use-library-panel": "Use library panel"
"menu-use-library-panel": "Use library panel",
"suggestions": {
"empty-state-message": "Run a query to start seeing suggested visualizations"
}
},
"new-panel-title": "New panel",
"on-create-new-row": {