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:
@@ -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),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user