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,
|
SceneObjectState,
|
||||||
SceneObjectStateChangedEvent,
|
SceneObjectStateChangedEvent,
|
||||||
SceneQueryRunner,
|
SceneQueryRunner,
|
||||||
|
sceneGraph,
|
||||||
sceneUtils,
|
sceneUtils,
|
||||||
VizPanel,
|
VizPanel,
|
||||||
isSceneObject,
|
isSceneObject,
|
||||||
@@ -21,6 +22,7 @@ import { Panel } from '@grafana/schema';
|
|||||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||||
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
||||||
|
import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions';
|
||||||
|
|
||||||
import { DashboardEditActionEvent } from '../edit-pane/shared';
|
import { DashboardEditActionEvent } from '../edit-pane/shared';
|
||||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
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 { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||||
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
||||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||||
|
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||||
import {
|
import {
|
||||||
activateSceneObjectAndParentTree,
|
activateSceneObjectAndParentTree,
|
||||||
getDashboardSceneFor,
|
getDashboardSceneFor,
|
||||||
@@ -84,7 +87,11 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
const panel = this.state.panelRef.resolve();
|
const panel = this.state.panelRef.resolve();
|
||||||
|
|
||||||
if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
|
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(
|
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() {
|
private commitChanges() {
|
||||||
if (!this.state.isDirty && !this._changesHaveBeenMade) {
|
if (!this.state.isDirty && !this._changesHaveBeenMade) {
|
||||||
// Nothing to commit
|
// 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
|
// Setup options pane
|
||||||
this.setState({
|
this.setState({
|
||||||
optionsPane: new PanelOptionsPane({
|
optionsPane: new PanelOptionsPane({
|
||||||
panelRef: this.state.panelRef,
|
panelRef: this.state.panelRef,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
listMode: OptionFilter.All,
|
listMode: OptionFilter.All,
|
||||||
|
isVizPickerOpen: isUnconfigured,
|
||||||
}),
|
}),
|
||||||
isInitializing: false,
|
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 { Trans, t } from '@grafana/i18n';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import { sceneUtils } from '@grafana/scenes';
|
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 { DashboardInteractions } from '../utils/interactions';
|
||||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
|
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ const UnconfiguredPanel = new PanelPlugin(UnconfiguredPanelComp);
|
|||||||
|
|
||||||
function UnconfiguredPanelComp(props: PanelProps) {
|
function UnconfiguredPanelComp(props: PanelProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const panelContext = usePanelContext();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const onMenuClick = useCallback((isOpen: boolean) => {
|
const onMenuClick = useCallback((isOpen: boolean) => {
|
||||||
setIsOpen(isOpen);
|
setIsOpen(isOpen);
|
||||||
@@ -26,14 +30,15 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
|||||||
DashboardInteractions.panelActionClicked('configure', props.id, 'panel');
|
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');
|
throw new Error('DashboardScene not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id));
|
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
throw new Error('Panel not found');
|
throw new Error('Panel not found');
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,18 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
|||||||
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
|
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 = () => (
|
const MenuActions = () => (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -56,6 +73,25 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
|||||||
</Menu>
|
</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 (
|
return (
|
||||||
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
|
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
|
||||||
<Box paddingBottom={2}>
|
<Box paddingBottom={2}>
|
||||||
@@ -79,3 +115,20 @@ sceneUtils.registerRuntimePanelPlugin({
|
|||||||
pluginId: UNCONFIGURED_PANEL_PLUGIN_ID,
|
pluginId: UNCONFIGURED_PANEL_PLUGIN_ID,
|
||||||
plugin: UnconfiguredPanel,
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
|
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return null;
|
return null;
|
||||||
@@ -258,8 +259,15 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultVizPanel(): VizPanel {
|
export function getDefaultVizPanel(): VizPanel {
|
||||||
const defaultPluginId = config.featureToggles.dashboardNewLayouts ? UNCONFIGURED_PANEL_PLUGIN_ID : 'timeseries';
|
const defaultPluginId =
|
||||||
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
|
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({
|
return new VizPanel({
|
||||||
title: newPanelTitle,
|
title: newPanelTitle,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
|
|
||||||
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
||||||
import { Trans } from '@grafana/i18n';
|
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';
|
import { getAllSuggestions } from '../../suggestions/getAllSuggestions';
|
||||||
|
|
||||||
@@ -31,6 +32,21 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel, t
|
|||||||
return result;
|
return result;
|
||||||
}, [searchQuery, suggestions, trackSearch]);
|
}, [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 (
|
return (
|
||||||
// This div is needed in some places to make AutoSizer work
|
// This div is needed in some places to make AutoSizer work
|
||||||
<div>
|
<div>
|
||||||
@@ -116,5 +132,18 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
justifyContent: 'space-evenly',
|
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": {
|
"new-panel": {
|
||||||
"configure-button": "Configure",
|
"configure-button": "Configure",
|
||||||
"configure-button-menu": "Toggle menu",
|
"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-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",
|
"new-panel-title": "New panel",
|
||||||
"on-create-new-row": {
|
"on-create-new-row": {
|
||||||
|
|||||||
Reference in New Issue
Block a user