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
This commit is contained in:
Torkel Ödegaard
2024-04-05 11:24:25 +02:00
committed by GitHub
parent 6a53864f7a
commit 49a3a95dd1
5 changed files with 275 additions and 204 deletions
@@ -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 = <PanelOptions vizManager={vizManager} searchQuery="" listMode={OptionFilter.All}></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 = (
<PanelOptions vizManager={panelManger} searchQuery="" listMode={OptionFilter.All}></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'
);
});
@@ -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;
@@ -393,6 +393,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
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<VizPanelManagerState> {
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<VizPanelManager>) => {
const { panel, tableView } = model.useState();
const styles = useStyles2(getStyles);
@@ -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 <PanelFrameTitle vizManager={vizManager} />;
},
addon: config.featureToggles.dashgpt && (
<GenAIPanelTitleButton
onGenerate={(title) => vizManager.setPanelTitle(title)}
panel={vizPanelToPanel(panel)}
dashboard={transformSceneToSaveModel(dashboard)}
/>
),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Description',
value: panel.state.description,
render: function renderDescription() {
return <DescriptionTextArea panel={panel} />;
},
addon: config.featureToggles.dashgpt && (
<GenAIPanelDescriptionButton
onGenerate={(description) => panel.setState({ description })}
panel={vizPanelToPanel(panel)}
/>
),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Transparent background',
render: function renderTransparent() {
return (
<Switch
value={panel.state.displayMode === 'transparent'}
id="transparent-background"
onChange={() => {
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: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
})
)
)
.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 (
<RepeatRowSelect2
id="repeat-by-variable-select"
parent={panel}
repeat={repeat}
onChange={(value?: string) => {
const stateUpdate: Partial<VizPanelManagerState> = { 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<SelectableValue<'h' | 'v'>> = [
{ label: 'Horizontal', value: 'h' },
{ label: 'Vertical', value: 'v' },
];
return (
<RadioButtonGroup
options={directionOptions}
value={vizManager.state.repeatDirection ?? 'h'}
onChange={(value) => 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 (
<Select
options={maxPerRowOptions}
value={vizManager.state.maxPerRow}
onChange={(value) => vizManager.setState({ maxPerRow: value.value })}
/>
);
},
})
)
);
}
interface ScenePanelLinksEditorProps {
panelLinks?: VizPanelLinks;
}
function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
const { rawLinks: links } = panelLinks ? panelLinks.useState() : { rawLinks: [] };
return (
<DataLinksInlineEditor
links={links}
onChange={(links) => panelLinks?.setState({ rawLinks: links })}
getSuggestions={getPanelLinksVariableSuggestions}
data={[]}
/>
);
}
function PanelFrameTitle({ vizManager }: { vizManager: VizPanelManager }) {
const { title } = vizManager.state.panel.useState();
return (
<Input
data-testid="panel-edit-panel-title-input"
value={title}
onChange={(e) => vizManager.setPanelTitle(e.currentTarget.value)}
/>
);
}
function DescriptionTextArea({ panel }: { panel: VizPanel }) {
const { description } = panel.useState();
return (
<TextArea
id="description-text-area"
value={description}
onChange={(e) => panel.setState({ description: e.currentTarget.value })}
/>
);
}
@@ -1,23 +1,13 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
import { VizPanelManager, VizPanelManagerState } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
import { VizPanelLinks } from 'app/features/dashboard-scene/scene/PanelLinks';
import {
transformSceneToSaveModel,
vizPanelToPanel,
} from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils';
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
import { GenAIPanelDescriptionButton } from '../GenAI/GenAIPanelDescriptionButton';
import { GenAIPanelTitleButton } from '../GenAI/GenAIPanelTitleButton';
import { RepeatRowSelect, RepeatRowSelect2 } from '../RepeatRowSelect/RepeatRowSelect';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
@@ -191,187 +181,3 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
)
);
}
export function getPanelFrameCategory2(
panelManager: 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 <PanelFrameTitle panel={panel} />;
},
addon: config.featureToggles.dashgpt && (
<GenAIPanelTitleButton
onGenerate={(title) => panel.setState({ title })}
panel={vizPanelToPanel(panel)}
dashboard={transformSceneToSaveModel(dashboard)}
/>
),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Description',
value: panel.state.description,
render: function renderDescription() {
return <DescriptionTextArea panel={panel} />;
},
addon: config.featureToggles.dashgpt && (
<GenAIPanelDescriptionButton
onGenerate={(description) => panel.setState({ description })}
panel={vizPanelToPanel(panel)}
/>
),
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Transparent background',
render: function renderTransparent() {
return (
<Switch
value={panel.state.displayMode === 'transparent'}
id="transparent-background"
onChange={() => {
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: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
})
)
)
.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 (
<RepeatRowSelect2
id="repeat-by-variable-select"
parent={panel}
repeat={repeat}
onChange={(value?: string) => {
const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
if (value && !panelManager.state.repeatDirection) {
stateUpdate.repeatDirection = 'h';
}
panelManager.setState(stateUpdate);
}}
/>
);
},
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Repeat direction',
showIf: () => !!panelManager.state.repeat,
render: function renderRepeatOptions() {
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
{ label: 'Horizontal', value: 'h' },
{ label: 'Vertical', value: 'v' },
];
return (
<RadioButtonGroup
options={directionOptions}
value={panelManager.state.repeatDirection ?? 'h'}
onChange={(value) => panelManager.setState({ repeatDirection: value })}
/>
);
},
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Max per row',
showIf: () => Boolean(panelManager.state.repeat && panelManager.state.repeatDirection === 'h'),
render: function renderOption() {
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
return (
<Select
options={maxPerRowOptions}
value={panelManager.state.maxPerRow}
onChange={(value) => panelManager.setState({ maxPerRow: value.value })}
/>
);
},
})
)
);
}
interface ScenePanelLinksEditorProps {
panelLinks?: VizPanelLinks;
}
function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
const { rawLinks: links } = panelLinks ? panelLinks.useState() : { rawLinks: [] };
return (
<DataLinksInlineEditor
links={links}
onChange={(links) => panelLinks?.setState({ rawLinks: links })}
getSuggestions={getPanelLinksVariableSuggestions}
data={[]}
/>
);
}
function PanelFrameTitle({ panel }: { panel: VizPanel }) {
const { title } = panel.useState();
return (
<Input id="PanelFrameTitle" value={title} onChange={(e) => panel.setState({ title: e.currentTarget.value })} />
);
}
function DescriptionTextArea({ panel }: { panel: VizPanel }) {
const { description } = panel.useState();
return (
<TextArea
id="description-text-area"
value={description}
onChange={(e) => panel.setState({ description: e.currentTarget.value })}
/>
);
}