From e3bc61e7d26149b8a75a44e24f8bb9437f686cc0 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 31 Dec 2025 10:56:47 -0500 Subject: [PATCH] Suggestions: Add intermediate state to avoid unexpected saved states (#115709) * Suggestions: Add intermediate state to avoid unexpected saved states * cleanup * update and add e2es to confirm behavior * fix some of the change dispatch * codeowners * fix js error that this exposed * Apply suggestion from @fastfrwrd --- .github/CODEOWNERS | 2 +- .../smoke-tests-suite/panels.spec.ts | 4 +- .../visualization-suggestions-v2.spec.ts | 178 ++++++++++++++++++ .../visualization-suggestions.spec.ts | 2 +- .../src/selectors/components.ts | 3 + .../core/components/TimelineChart/utils.ts | 2 +- .../panel-edit/PanelEditor.tsx | 42 ++++- .../panel-edit/PanelEditorRenderer.tsx | 4 +- .../panel-edit/PanelOptionsPane.test.tsx | 12 +- .../panel-edit/PanelOptionsPane.tsx | 11 +- .../panel-edit/PanelVizTypePicker.tsx | 85 +++++---- .../VisualizationSuggestions.tsx | 59 ++++-- 12 files changed, 323 insertions(+), 81 deletions(-) create mode 100644 e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a8dc5d1a68b..ef476f4d272 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -519,7 +519,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /e2e-playwright/various-suite/solo-route.spec.ts @grafana/dashboards-squad /e2e-playwright/various-suite/trace-view-scrolling.spec.ts @grafana/observability-traces-and-profiling /e2e-playwright/various-suite/verify-i18n.spec.ts @grafana/grafana-frontend-platform -/e2e-playwright/various-suite/visualization-suggestions.spec.ts @grafana/dataviz-squad +/e2e-playwright/various-suite/visualization-suggestions*.spec.ts @grafana/dataviz-squad /e2e-playwright/various-suite/perf-test.spec.ts @grafana/grafana-frontend-platform # Packages diff --git a/e2e-playwright/smoke-tests-suite/panels.spec.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts index 22f6088d99d..0ad50a1aed7 100644 --- a/e2e-playwright/smoke-tests-suite/panels.spec.ts +++ b/e2e-playwright/smoke-tests-suite/panels.spec.ts @@ -1,4 +1,4 @@ -import { BootData } from '@grafana/data'; +import { BootData, PanelPluginMeta } from '@grafana/data'; import { test, expect } from '@grafana/plugin-e2e'; test.describe( @@ -22,7 +22,7 @@ test.describe( await dashboardPage.addPanel(); // Get panel types from window object - const panelTypes = await page.evaluate(() => { + const panelTypes: PanelPluginMeta[] = await page.evaluate(() => { // @grafana/plugin-e2e doesn't export the full bootdata config // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const win = window as typeof window & { grafanaBootData: BootData }; diff --git a/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts b/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts new file mode 100644 index 00000000000..282ec1314cc --- /dev/null +++ b/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts @@ -0,0 +1,178 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newVizSuggestions: true, + externalVizSuggestions: false, + }, + viewport: { + width: 800, + height: 1500, + }, +}); + +test.describe( + 'Visualization suggestions v2', + { + tag: ['@various', '@suggestions'], + }, + () => { + test('Should be shown and clickable', async ({ selectors, gotoPanelEditPage }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel' + ).toBeVisible(); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // TODO: in this part of the test, we will change the query and the transforms and observe suggestions being updated. + + // Select a visualization and verify table header is visible from preview + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button disabled since panel has not yet changed' + ).toBeDisabled(); + + // apply the suggestion and verify panel options are visible + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.confirm('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), + 'options pane to be rendered' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button enabled now that panel is dirty' + ).toBeEnabled(); + }); + + test('should not apply suggestion if you navigate toggle the viz picker back off', async ({ + selectors, + gotoPanelEditPage, + }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel;' + ).toBeVisible(); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton) + ).toBeDisabled(); + + // Verify that toggling the viz picker back cancels the suggestion, restores the line chart, shows panel options + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), + 'options pane to be rendered' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button is still disabled since no changes were applied' + ).toBeDisabled(); + }); + + test('should not apply suggestion if you navigate back to the dashboard', async ({ + page, + selectors, + gotoPanelEditPage, + }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect(page.getByRole('grid').getByRole('row').first(), 'table row to be rendered').toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton) + ).toBeDisabled(); + + // Verify that navigating back to the dashboard cancels the suggestion and restores the line chart. + await panelEditPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + page.locator('[data-viz-panel-key="panel-9"]').locator('.uplot'), + 'time series to be rendered inside the panel' + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts index d01447b7eb7..7872e5a69d0 100644 --- a/e2e-playwright/various-suite/visualization-suggestions.spec.ts +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.describe( 'Visualization suggestions', { - tag: ['@various'], + tag: ['@various', '@suggestions'], }, () => { test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => { diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 0e7e7bf6150..7dd819a1512 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -1293,6 +1293,9 @@ export const versionedComponents = { card: { [MIN_GRAFANA_VERSION]: (name: string) => `data-testid suggestion-${name}`, }, + confirm: { + '12.4.0': (name: string) => `data-testid suggestion-${name} confirm button`, + }, }, ColorSwatch: { name: { diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 264c5498be9..df0d21ebda8 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -192,7 +192,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( }); const xField = frame.fields[0]; - const xAxisHidden = xField.config.custom.axisPlacement === AxisPlacement.Hidden; + const xAxisHidden = xField.config.custom?.axisPlacement === AxisPlacement.Hidden; builder.addAxis({ show: !xAxisHidden, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 497d58e505a..c5566fdab38 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -51,6 +51,7 @@ export interface PanelEditorState extends SceneObjectState { panelRef: SceneObjectRef; showLibraryPanelSaveModal?: boolean; showLibraryPanelUnlinkModal?: boolean; + editPreview?: VizPanel; tableView?: VizPanel; pluginLoadErrror?: string; /** @@ -150,6 +151,9 @@ export class PanelEditor extends SceneObjectBase { const changedState = layoutItem.state; const originalState = this._layoutItemState!; + this.setState({ editPreview: undefined }); + this.state.optionsPane?.setState({ editPreviewRef: undefined }); + // Temp fix for old edit mode if (this._layoutItem instanceof DashboardGridItem && !config.featureToggles.dashboardNewLayouts) { this._layoutItem.handleEditChange(); @@ -256,16 +260,40 @@ export class PanelEditor extends SceneObjectBase { ); // Setup options pane + const optionsPane = new PanelOptionsPane({ + panelRef: this.state.panelRef, + editPreviewRef: this.state.editPreview?.getRef(), + searchQuery: '', + listMode: OptionFilter.All, + isVizPickerOpen: isUnconfigured, + isNewPanel: this.state.isNewPanel, + }); + this.setState({ - optionsPane: new PanelOptionsPane({ - panelRef: this.state.panelRef, - searchQuery: '', - listMode: OptionFilter.All, - isVizPickerOpen: isUnconfigured, - isNewPanel: this.state.isNewPanel, - }), + optionsPane, isInitializing: false, }); + + this._subs.add( + optionsPane.subscribeToState((newState, oldState) => { + if (newState.isVizPickerOpen !== oldState.isVizPickerOpen) { + const panel = this.state.panelRef.resolve(); + let editPreview: VizPanel | undefined; + if (newState.isVizPickerOpen) { + // we just "pick" timeseries, viz type will likely be overridden by Suggestions. + const editPreviewBuilder = PanelBuilders.timeseries() + .setTitle(panel.state.title) + .setDescription(panel.state.description); + if (panel.state.$data) { + editPreviewBuilder.setData(new DataProviderSharer({ source: panel.state.$data.getRef() })); + } + editPreview = editPreviewBuilder.build(); + } + this.setState({ editPreview }); + optionsPane.setState({ editPreviewRef: editPreview?.getRef() }); + } + }) + ); } else { // plugin changed after first time initialization // Just update data pane diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 30b52bf9260..1f1c3860571 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -81,7 +81,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) function VizAndDataPane({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); - const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView } = model.useState(); + const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView, editPreview } = model.useState(); const panel = model.getPanel(); const libraryPanel = getLibraryPanelBehavior(panel); const { controls } = dashboard.useState(); @@ -113,7 +113,7 @@ function VizAndDataPane({ model }: SceneComponentProps) { )}
- +
{showLibraryPanelSaveModal && libraryPanel && ( { expect(panel.state.pluginId).toBe('timeseries'); - optionsPane.onChangePanelPlugin({ pluginId: 'table' }); + optionsPane.onChangePanel({ pluginId: 'table' }); expect(optionsPane['_cachedPluginOptions']['timeseries']?.options).toBe(panel.state.options); expect(optionsPane['_cachedPluginOptions']['timeseries']?.fieldConfig).toBe(panel.state.fieldConfig); @@ -52,7 +52,7 @@ describe('PanelOptionsPane', () => { panel.setState({ $data: undefined }); panel.activate(); - optionsPane.onChangePanelPlugin({ + optionsPane.onChangePanel({ pluginId: 'table', options: { showHeader: false }, fieldConfig: { @@ -114,7 +114,7 @@ describe('PanelOptionsPane', () => { expect(panel.state.fieldConfig.overrides[1].properties).toHaveLength(1); expect(panel.state.fieldConfig.defaults.custom).toHaveProperty('axisBorderShow'); - optionsPane.onChangePanelPlugin({ pluginId: 'table' }); + optionsPane.onChangePanel({ pluginId: 'table' }); expect(mockFn).toHaveBeenCalled(); expect(mockFn.mock.calls[0][2].defaults.color?.mode).toBe('palette-classic'); @@ -146,8 +146,8 @@ describe('PanelOptionsPane', () => { const mockOnFieldConfigChange = jest.fn(); panel.onFieldConfigChange = mockOnFieldConfigChange; - // Call onChangePanelPlugin with fieldConfig that has overrides - optionsPane.onChangePanelPlugin({ + // Call onChangePanel with fieldConfig that has overrides + optionsPane.onChangePanel({ pluginId: 'table', fieldConfig: { defaults: { unit: 'percent' }, @@ -178,7 +178,7 @@ describe('PanelOptionsPane', () => { panel.onFieldConfigChange = mockOnFieldConfigChange; // Call without fieldConfig - optionsPane.onChangePanelPlugin({ + optionsPane.onChangePanel({ pluginId: 'table', options: { showHeader: false }, }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index 3f54971f99d..b863bf2fc78 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -41,6 +41,7 @@ export interface PanelOptionsPaneState extends SceneObjectState { panelRef: SceneObjectRef; isNewPanel?: boolean; hasPickedViz?: boolean; + editPreviewRef?: SceneObjectRef; } interface PluginOptionsCache { @@ -63,8 +64,7 @@ export class PanelOptionsPane extends SceneObjectBase { }); }; - onChangePanelPlugin = (options: VizTypeChangeDetails) => { - const panel = this.state.panelRef.resolve(); + onChangePanel = (options: VizTypeChangeDetails, panel = this.state.panelRef.resolve()) => { const { options: prevOptions, fieldConfig: prevFieldConfig, pluginId: prevPluginId } = panel.state; const pluginId = options.pluginId; @@ -137,8 +137,10 @@ export class PanelOptionsPane extends SceneObjectBase { } function PanelOptionsPaneComponent({ model }: SceneComponentProps) { - const { isVizPickerOpen, searchQuery, listMode, panelRef, isNewPanel, hasPickedViz } = model.useState(); + const { isVizPickerOpen, searchQuery, listMode, panelRef, isNewPanel, hasPickedViz, editPreviewRef } = + model.useState(); const panel = panelRef.resolve(); + const editPreview = editPreviewRef?.resolve() ?? panel; // if something goes wrong, at least update the panel. const { pluginId } = panel.useState(); const { data } = sceneGraph.getData(panel).useState(); const styles = useStyles2(getStyles); @@ -229,7 +231,8 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps void; + editPreview: VizPanel; + onChange: (options: VizTypeChangeDetails, panel?: VizPanel) => void; onClose: () => void; } @@ -41,7 +42,7 @@ const getTabs = (): Array<{ label: string; value: VisualizationSelectPaneTab }> : [allVisualizationsTab, suggestionsTab]; }; -export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackButton }: Props) { +export function PanelVizTypePicker({ panel, editPreview, data, onChange, onClose, showBackButton }: Props) { const styles = useStyles2(getStyles); const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]); const filterId = useId(); @@ -97,49 +98,55 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackBut - {listMode === VisualizationSelectPaneTab.Suggestions && ( - - )} - {listMode === VisualizationSelectPaneTab.Visualizations && ( - - - - {showBackButton && ( - - )} - - - + + + + {showBackButton && ( + + )} + + + + {listMode === VisualizationSelectPaneTab.Suggestions && ( + + )} + {listMode === VisualizationSelectPaneTab.Visualizations && ( - - )} + )} +
@@ -155,7 +162,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ gap: theme.spacing(2), }), searchField: css({ - marginTop: theme.spacing(0.5), // input glow with the boundary without this + margin: theme.spacing(0.5, 0, 1, 0), // input glow with the boundary without this }), tabs: css({ width: '100%', diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index d1763ad835f..924b5f3b6bf 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -9,8 +9,10 @@ import { PanelPluginMeta, PanelPluginVisualizationSuggestion, } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; import { Alert, Button, Icon, Spinner, Text, useStyles2 } from '@grafana/ui'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from 'app/features/dashboard-scene/scene/UnconfiguredPanel'; @@ -23,25 +25,47 @@ import { VisualizationSuggestionCard } from './VisualizationSuggestionCard'; import { VizTypeChangeDetails } from './types'; export interface Props { - onChange: (options: VizTypeChangeDetails) => void; + onChange: (options: VizTypeChangeDetails, panel?: VizPanel) => void; + editPreview?: VizPanel; data?: PanelData; panel?: PanelModel; + searchQuery?: string; } -const useSuggestions = (data: PanelData | undefined) => { +const useSuggestions = (data: PanelData | undefined, searchQuery: string | undefined) => { const [hasFetched, setHasFetched] = useState(false); const { value, loading, error, retry } = useAsyncRetry(async () => { await new Promise((resolve) => setTimeout(resolve, hasFetched ? 75 : 0)); setHasFetched(true); return await getAllSuggestions(data); }, [hasFetched, data]); - return { value, loading, error, retry }; + + const filteredValue = useMemo(() => { + if (!value || !searchQuery) { + return value; + } + + const lowerCaseQuery = searchQuery.toLowerCase(); + const filteredSuggestions = value.suggestions.filter( + (suggestion) => + suggestion.name.toLowerCase().includes(lowerCaseQuery) || + suggestion.pluginId.toLowerCase().includes(lowerCaseQuery) || + suggestion.description?.toLowerCase().includes(lowerCaseQuery) + ); + + return { + ...value, + suggestions: filteredSuggestions, + }; + }, [value, searchQuery]); + + return { value: filteredValue, loading, error, retry }; }; -export function VisualizationSuggestions({ onChange, data, panel }: Props) { +export function VisualizationSuggestions({ onChange, editPreview, data, panel, searchQuery }: Props) { const styles = useStyles2(getStyles); - const { value: result, loading, error, retry } = useSuggestions(data); + const { value: result, loading, error, retry } = useSuggestions(data, searchQuery); const suggestions = result?.suggestions; const hasLoadingErrors = result?.hasErrors ?? false; @@ -73,18 +97,21 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) { const applySuggestion = useCallback( (suggestion: PanelPluginVisualizationSuggestion, isPreview?: boolean) => { - onChange({ - pluginId: suggestion.pluginId, - options: suggestion.options, - fieldConfig: suggestion.fieldConfig, - withModKey: isPreview, - }); + onChange( + { + pluginId: suggestion.pluginId, + options: suggestion.options, + fieldConfig: suggestion.fieldConfig, + withModKey: isPreview, + }, + isPreview ? editPreview : undefined + ); if (isPreview) { setSuggestionHash(suggestion.hash); } }, - [onChange] + [onChange, editPreview] ); useEffect(() => { @@ -185,17 +212,13 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) { variant="primary" size={'md'} className={styles.applySuggestionButton} + data-testid={selectors.components.VisualizationPreview.confirm(suggestion.name)} aria-label={t( 'panel.visualization-suggestions.apply-suggestion-aria-label', 'Apply {{suggestionName}} visualization', { suggestionName: suggestion.name } )} - onClick={() => - onChange({ - pluginId: suggestion.pluginId, - withModKey: false, - }) - } + onClick={() => applySuggestion(suggestion, false)} > {t('panel.visualization-suggestions.use-this-suggestion', 'Use this suggestion')}