From 49a3a95dd19dbfc4b758bbcc7b7418ebc9ebed43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 5 Apr 2024 11:24:25 +0200 Subject: [PATCH] DashboardScene: Fixes panel edit issue with clearing title not resulting in hover header mode (#85621) * PanelEditor: Update hoverHeader state when changing panel title * refactor test and remove duplicate --- .../panel-edit/PanelOptions.test.tsx | 69 +++++- .../panel-edit/PanelOptions.tsx | 2 +- .../panel-edit/VizPanelManager.tsx | 5 + .../panel-edit/getPanelFrameOptions.tsx | 207 ++++++++++++++++++ .../PanelEditor/getPanelFrameOptions.tsx | 196 +---------------- 5 files changed, 275 insertions(+), 204 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index e2ff676701f..973d6a433fb 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -1,6 +1,7 @@ -import { act, fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { selectors } from '@grafana/e2e-selectors'; import { VizPanel } from '@grafana/scenes'; import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; @@ -13,6 +14,8 @@ import * as utils from '../utils/utils'; import { PanelOptions } from './PanelOptions'; import { VizPanelManager } from './VizPanelManager'; +const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane; + jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '', @@ -22,7 +25,61 @@ jest.mock('react-router-dom', () => ({ // Needed when the panel is not part of an DashboardScene jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(new DashboardScene({})); +interface SetupOptions { + panel?: VizPanel; +} + +function setup(options: SetupOptions = {}) { + let panel = options.panel; + + if (!panel) { + panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + title: 'My title', + }); + + new DashboardGridItem({ body: panel }); + } + + const vizManager = VizPanelManager.createFor(panel); + + const panelOptions = ; + + const renderResult = render(panelOptions); + + return { renderResult, vizManager }; +} + describe('PanelOptions', () => { + describe('Can render and edit panel frame options', () => { + it('Can edit title', async () => { + const { vizManager } = setup(); + + expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument(); + + const input = screen.getByTestId('panel-edit-panel-title-input'); + fireEvent.change(input, { target: { value: 'New title' } }); + + expect(vizManager.state.panel.state.title).toBe('New title'); + }); + + it('Clearing title should set hoverHeader to true', async () => { + const { vizManager } = setup(); + + expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument(); + + const input = screen.getByTestId('panel-edit-panel-title-input'); + fireEvent.change(input, { target: { value: '' } }); + + expect(vizManager.state.panel.state.title).toBe(''); + expect(vizManager.state.panel.state.hoverHeader).toBe(true); + + fireEvent.change(input, { target: { value: 'Muu' } }); + expect(vizManager.state.panel.state.hoverHeader).toBe(false); + }); + }); + it('gets library panel options when the editing a library panel', async () => { const panel = new VizPanel({ key: 'panel-1', @@ -50,19 +107,15 @@ describe('PanelOptions', () => { new DashboardGridItem({ body: libraryPanel }); - const panelManger = VizPanelManager.createFor(panel); + const { renderResult, vizManager } = setup({ panel: panel }); - const panelOptions = ( - - ); + const input = await renderResult.findByTestId('library panel name input'); - const r = render(panelOptions); - const input = await r.findByTestId('library panel name input'); await act(async () => { fireEvent.blur(input, { target: { value: 'new library panel name' } }); }); - expect((panelManger.state.sourcePanel.resolve().parent as LibraryVizPanel).state.name).toBe( + expect((vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).state.name).toBe( 'new library panel name' ); }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index 2c0c4a4a2b4..a7f68110266 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -3,7 +3,6 @@ import React, { useMemo } from 'react'; import { PanelData } from '@grafana/data'; import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements'; -import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions'; import { getLibraryVizPanelOptionsCategory, getVisualizationOptions2, @@ -12,6 +11,7 @@ import { import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelManager } from './VizPanelManager'; +import { getPanelFrameCategory2 } from './getPanelFrameOptions'; interface Props { vizManager: VizPanelManager; diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 47357312c9f..17a61fd5df2 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -393,6 +393,7 @@ export class VizPanelManager extends SceneObjectBase { repeatDirection: this.state.repeatDirection, maxPerRow: this.state.maxPerRow, }; + if (sourcePanel.parent instanceof DashboardGridItem) { sourcePanel.parent.setState({ ...repeatUpdate, @@ -448,6 +449,10 @@ export class VizPanelManager extends SceneObjectBase { return this.state.panel.clone({ $data: this.state.$data?.clone() }); } + public setPanelTitle(newTitle: string) { + this.state.panel.setState({ title: newTitle, hoverHeader: newTitle === '' }); + } + public static Component = ({ model }: SceneComponentProps) => { const { panel, tableView } = model.useState(); const styles = useStyles2(getStyles); diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx new file mode 100644 index 00000000000..f3d0350775e --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -0,0 +1,207 @@ +import React from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; +import { RadioButtonGroup, Select, DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui'; +import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton'; +import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; + +import { VizPanelLinks } from '../scene/PanelLinks'; +import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { VizPanelManager, VizPanelManagerState } from './VizPanelManager'; + +export function getPanelFrameCategory2( + vizManager: VizPanelManager, + panel: VizPanel, + repeat?: string +): OptionsPaneCategoryDescriptor { + const descriptor = new OptionsPaneCategoryDescriptor({ + title: 'Panel options', + id: 'Panel options', + isOpenDefault: true, + }); + + const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel); + const links = panelLinksObject?.state.rawLinks ?? []; + const dashboard = getDashboardSceneFor(panel); + + return descriptor + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Title', + value: panel.state.title, + popularRank: 1, + render: function renderTitle() { + return ; + }, + addon: config.featureToggles.dashgpt && ( + vizManager.setPanelTitle(title)} + panel={vizPanelToPanel(panel)} + dashboard={transformSceneToSaveModel(dashboard)} + /> + ), + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Description', + value: panel.state.description, + render: function renderDescription() { + return ; + }, + addon: config.featureToggles.dashgpt && ( + panel.setState({ description })} + panel={vizPanelToPanel(panel)} + /> + ), + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Transparent background', + render: function renderTransparent() { + return ( + { + panel.setState({ + displayMode: panel.state.displayMode === 'transparent' ? 'default' : 'transparent', + }); + }} + /> + ); + }, + }) + ) + .addCategory( + new OptionsPaneCategoryDescriptor({ + title: 'Panel links', + id: 'Panel links', + isOpenDefault: false, + itemsCount: links?.length, + }).addItem( + new OptionsPaneItemDescriptor({ + title: 'Panel links', + render: () => , + }) + ) + ) + .addCategory( + new OptionsPaneCategoryDescriptor({ + title: 'Repeat options', + id: 'Repeat options', + isOpenDefault: false, + }) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Repeat by variable', + description: + 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.', + render: function renderRepeatOptions() { + return ( + { + const stateUpdate: Partial = { repeat: value }; + if (value && !vizManager.state.repeatDirection) { + stateUpdate.repeatDirection = 'h'; + } + vizManager.setState(stateUpdate); + }} + /> + ); + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Repeat direction', + showIf: () => !!vizManager.state.repeat, + render: function renderRepeatOptions() { + const directionOptions: Array> = [ + { label: 'Horizontal', value: 'h' }, + { label: 'Vertical', value: 'v' }, + ]; + + return ( + vizManager.setState({ repeatDirection: value })} + /> + ); + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Max per row', + showIf: () => Boolean(vizManager.state.repeat && vizManager.state.repeatDirection === 'h'), + render: function renderOption() { + const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value })); + return ( + vizManager.setPanelTitle(e.currentTarget.value)} + /> + ); +} + +function DescriptionTextArea({ panel }: { panel: VizPanel }) { + const { description } = panel.useState(); + + return ( +