diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 1fe646ca69b..1ff7eef89ca 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -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 { 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 { }; } + 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 { }) ); + 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, }); diff --git a/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx b/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx index eda81bf019a..2be7201ad74 100644 --- a/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx +++ b/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx @@ -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 = () => ( ); + const showEmptyState = config.featureToggles.newVizSuggestions && panelContext.app === CoreApp.PanelEditor; + + if (showEmptyState) { + const defaultContent = ( + + Run a query to visualize it here or go to all visualizations to add other panel types + + ); + + return ( +
+ + + {defaultContent} + +
+ ); + } + return ( @@ -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), + }), + }; +} diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7691b52f3b8..7188756b055 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -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, diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index 46e5bdbe012..46232beff41 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -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 ( +
+ + + + Run a query to start seeing suggested visualizations + + +
+ ); + } + return ( // This div is needed in some places to make AutoSizer work
@@ -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), + }), }; }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0c9b8b63682..874be92a233 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {