From 1576b69f65a02833bcee2e6fc761cbbfc7f7f65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Mar 2025 08:35:50 +0100 Subject: [PATCH] Dashboards: Select objects after adding them (#101609) * Dashboards: Select newly added object * Add tests * Update --- .../edit-pane/DashboardEditPane.tsx | 38 +++++++++---------- .../edit-pane/useEditableElement.ts | 10 +++-- .../scene/DashboardScene.test.tsx | 15 ++++++++ .../dashboard-scene/scene/DashboardScene.tsx | 13 +++++-- .../scene/layout-rows/RowItemEditor.tsx | 30 ++++++++++++--- .../scene/layout-rows/RowItemRenderer.tsx | 2 +- .../scene/layout-rows/RowsLayoutManager.tsx | 6 ++- .../scene/layout-tabs/TabsLayoutManager.tsx | 5 ++- .../scene/layouts-shared/addNew.ts | 30 ++++++++------- public/locales/en-US/grafana.json | 20 +++++----- public/locales/pseudo-LOCALE/grafana.json | 20 +++++----- 11 files changed, 119 insertions(+), 70 deletions(-) diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 51cd262b24a..5a397991f74 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -15,7 +15,6 @@ import { import { t } from 'app/core/internationalization'; import { isInCloneChain } from '../utils/clone'; -import { getDashboardSceneFor } from '../utils/utils'; import { DashboardAddPane } from './DashboardAddPane'; import { DashboardOutline } from './DashboardOutline'; @@ -72,22 +71,20 @@ export class DashboardEditPane extends SceneObjectBase { } public selectObject(obj: SceneObject, id: string, multi?: boolean) { - if (!this.state.selection) { - return; - } - - const prevItem = this.state.selection.getFirstObject(); + const prevItem = this.state.selection?.getFirstObject(); if (prevItem === obj && !multi) { this.clearSelection(); return; } - if (multi && this.state.selection.hasValue(id)) { + if (multi && this.state.selection?.hasValue(id)) { this.removeMultiSelectedObject(id); return; } - const { selection, contextItems: selected } = this.state.selection.getStateWithValue(id, obj, !!multi); + const elementSelection = this.state.selection ?? new ElementSelection([[id, obj.getRef()]]); + + const { selection, contextItems: selected } = elementSelection.getStateWithValue(id, obj, !!multi); this.setState({ selection: new ElementSelection(selection), @@ -120,14 +117,12 @@ export class DashboardEditPane extends SceneObjectBase { } public clearSelection() { - const dashboard = getDashboardSceneFor(this); - - if (this.state.selection?.getFirstObject() === dashboard) { + if (!this.state.selection) { return; } this.setState({ - selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]), + selection: undefined, selectionContext: { ...this.state.selectionContext, selected: [], @@ -138,6 +133,14 @@ export class DashboardEditPane extends SceneObjectBase { public onChangeTab = (tab: EditPaneTab) => { this.setState({ tab }); }; + + public newObjectAddedToCanvas(obj: SceneObject) { + this.selectObject(obj, obj.state.key!, false); + + if (this.state.tab !== 'configure') { + this.onChangeTab('configure'); + } + } } export interface Props { @@ -153,13 +156,6 @@ export interface Props { export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse, openOverlay }: Props) { // Activate the edit pane useEffect(() => { - if (!editPane.state.selection) { - const dashboard = getDashboardSceneFor(editPane); - editPane.setState({ - selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]), - }); - } - editPane.enableSelection(); return () => { @@ -168,7 +164,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla }, [editPane]); useEffect(() => { - if (isCollapsed && editPane.state.selection?.getSelectionEntries().length) { + if (isCollapsed) { editPane.clearSelection(); } }, [editPane, isCollapsed]); @@ -176,7 +172,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla const { selection, tab = 'configure' } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const styles = useStyles2(getStyles); const paneRef = useRef(null); - const editableElement = useEditableElement(selection); + const editableElement = useEditableElement(selection, editPane); if (!editableElement) { return null; diff --git a/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts b/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts index 2d6c79ff0f0..7a5aac72ec2 100644 --- a/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts +++ b/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts @@ -2,17 +2,21 @@ import { useMemo } from 'react'; import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; +import { getDashboardSceneFor } from '../utils/utils'; +import { DashboardEditPane } from './DashboardEditPane'; import { ElementSelection } from './ElementSelection'; export function useEditableElement( - selection: ElementSelection | undefined + selection: ElementSelection | undefined, + editPane: DashboardEditPane ): EditableDashboardElement | MultiSelectedEditableDashboardElement | undefined { return useMemo(() => { if (!selection) { - return undefined; + const dashboard = getDashboardSceneFor(editPane); + return new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]).createSelectionElement(); } return selection.createSelectionElement(); - }, [selection]); + }, [selection, editPane]); } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 386aa1e7782..4a02c6d8d26 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -452,6 +452,21 @@ describe('DashboardScene', () => { expect(panel.state.key).toBe('panel-7'); }); + it('Should select new panel', () => { + const panel = scene.onCreateNewPanel(); + expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(panel); + }); + + it('Should select new row', () => { + const row = scene.onCreateNewRow(); + expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(row); + }); + + it('Should select new tab', () => { + const tab = scene.onCreateNewTab(); + expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(tab); + }); + it('Should fail to copy a panel if it does not have a grid item parent', () => { const vizPanel = new VizPanel({ title: 'Panel Title', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 81c0acacd8f..e4a9ce06d4b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -488,7 +488,10 @@ export class DashboardScene extends SceneObjectBase impleme this.onEnterEditMode(); } + // Add panel to layout this.state.body.addPanel(vizPanel); + // Select panel + this.state.editPane.newObjectAddedToCanvas(vizPanel); } public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) { @@ -596,18 +599,20 @@ export class DashboardScene extends SceneObjectBase impleme } public onCreateNewRow() { - addNewRowTo(this.state.body); + const newRow = addNewRowTo(this.state.body); + this.state.editPane.newObjectAddedToCanvas(newRow); + return newRow; } public onCreateNewTab() { - addNewTabTo(this.state.body); + const tab = addNewTabTo(this.state.body); + this.state.editPane.newObjectAddedToCanvas(tab); + return tab; } public onCreateNewPanel(): VizPanel { const vizPanel = getDefaultVizPanel(); - this.addPanel(vizPanel); - return vizPanel; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index 3ff3181129d..62e1a7c4949 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react'; +import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Alert, Input, Switch, TextLink } from '@grafana/ui'; +import { Alert, Input, RadioButtonGroup, Switch, TextLink } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -22,19 +23,25 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] const dashboard = getDashboardSceneFor(model); const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ - title: t('dashboard.rows-layout.row-options.title', 'Row'), + title: t('dashboard.rows-layout.item-name', 'Row'), id: 'row-options', isOpenable: false, renderTitle: () => ( - model.onDelete()} /> + model.onDelete()} /> ), }) .addItem( new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.row-options.title-option', 'Title'), + title: t('dashboard.rows-layout.option.title', 'Title'), render: () => , }) ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.option.height', 'Height'), + render: () => , + }) + ) .addItem( new OptionsPaneItemDescriptor({ title: t('dashboard.layout.common.layout', 'Layout'), @@ -51,13 +58,13 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] editPaneHeaderOptions .addItem( new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat for'), + title: t('dashboard.rows-layout.option.repeat', 'Repeat for'), render: () => , }) ) .addItem( new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.row-options.height.hide-row-header', 'Hide row header'), + title: t('dashboard.rows-layout.option.hide-header', 'Hide row header'), render: () => , }) ); @@ -86,6 +93,17 @@ function RowHeaderSwitch({ row }: { row: RowItem }) { return row.onHeaderHiddenToggle()} />; } +function RowHeightSelect({ row }: { row: RowItem }) { + const { height = 'min' } = row.useState(); + + const options: Array> = [ + { label: t('dashboard.rows-layout.options.height-expand', 'Expand'), value: 'expand' }, + { label: t('dashboard.rows-layout.options.height-min', 'Min'), value: 'min' }, + ]; + + return row.onChangeHeight(option)} />; +} + function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) { const { layout } = row.useState(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index 97c78ae938b..2e170df5dcd 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -14,7 +14,7 @@ import { RowItem } from './RowItem'; import { RowItemMenu } from './RowItemMenu'; export function RowItemRenderer({ model }: SceneComponentProps) { - const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState(); + const { layout, title, isCollapsed, height = 'min', isHeaderHidden, key } = model.useState(); const isClone = useMemo(() => isClonedKey(key!), [key]); const dashboard = getDashboardSceneFor(model); const { isEditing, showHiddenElements } = dashboard.useState(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 5acf9063531..3ef34cbbc0f 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -69,8 +69,10 @@ export class RowsLayoutManager extends SceneObjectBase i throw new Error('Method not implemented.'); } - public addNewRow() { - this.setState({ rows: [...this.state.rows, new RowItem()] }); + public addNewRow(): RowItem { + const row = new RowItem(); + this.setState({ rows: [...this.state.rows, row] }); + return row; } public editModeChanged(isEditing: boolean) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 410fe06d781..0628850ed68 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -86,8 +86,9 @@ export class TabsLayoutManager extends SceneObjectBase i } public addNewTab() { - const currentTab = new TabItem(); - this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length }); + const newTab = new TabItem(); + this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length }); + return newTab; } public editModeChanged(isEditing: boolean) { diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts index 11385b322a2..68827d0ab11 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts @@ -1,14 +1,15 @@ -import { SceneObject } from '@grafana/scenes'; +import { SceneGridRow, SceneObject } from '@grafana/scenes'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; +import { RowItem } from '../layout-rows/RowItem'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; +import { TabItem } from '../layout-tabs/TabItem'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { isLayoutParent } from '../types/LayoutParent'; -export function addNewTabTo(sceneObject: SceneObject) { +export function addNewTabTo(sceneObject: SceneObject): TabItem { if (sceneObject instanceof TabsLayoutManager) { - sceneObject.addNewTab(); - return; + return sceneObject.addNewTab(); } const layoutParent = sceneObject.parent!; @@ -16,24 +17,24 @@ export function addNewTabTo(sceneObject: SceneObject) { throw new Error('Parent layout is not a LayoutParent'); } - layoutParent.switchLayout(TabsLayoutManager.createFromLayout(layoutParent.getLayout())); + const tabsLayout = TabsLayoutManager.createFromLayout(layoutParent.getLayout()); + layoutParent.switchLayout(tabsLayout); + + return tabsLayout.state.tabs[0]; } -export function addNewRowTo(sceneObject: SceneObject) { +export function addNewRowTo(sceneObject: SceneObject): RowItem | SceneGridRow { if (sceneObject instanceof RowsLayoutManager) { - sceneObject.addNewRow(); - return; + return sceneObject.addNewRow(); } if (sceneObject instanceof DefaultGridLayoutManager) { - sceneObject.addNewRow(); - return; + return sceneObject.addNewRow(); } if (sceneObject instanceof TabsLayoutManager) { const currentTab = sceneObject.getCurrentTab(); - addNewRowTo(currentTab.state.layout); - return; + return addNewRowTo(currentTab.state.layout); } const layoutParent = sceneObject.parent!; @@ -41,5 +42,8 @@ export function addNewRowTo(sceneObject: SceneObject) { throw new Error('Parent layout is not a LayoutParent'); } - layoutParent.switchLayout(RowsLayoutManager.createFromLayout(layoutParent.getLayout())); + const rowsLayout = RowsLayoutManager.createFromLayout(layoutParent.getLayout()); + layoutParent.switchLayout(rowsLayout); + + return rowsLayout.state.rows[0]; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dd2c244c452..3e89b9d327d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1214,7 +1214,18 @@ }, "rows-layout": { "description": "Rows layout", + "item-name": "Row", "name": "Rows", + "option": { + "height": "Height", + "hide-header": "Hide row header", + "repeat": "Repeat for", + "title": "Title" + }, + "options": { + "height-expand": "Expand", + "height-min": "Min" + }, "row": { "collapse": "Collapse row", "expand": "Expand row", @@ -1234,15 +1245,6 @@ } }, "row-options": { - "height": { - "hide-row-header": "Hide row header" - }, - "repeat": { - "variable": { - "title": "Repeat for" - } - }, - "title": "Row", "title-option": "Title" } }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index e92128c86f6..f2cb3b3942f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1214,7 +1214,18 @@ }, "rows-layout": { "description": "Ŗőŵş ľäyőūŧ", + "item-name": "Ŗőŵ", "name": "Ŗőŵş", + "option": { + "height": "Ħęįģĥŧ", + "hide-header": "Ħįđę řőŵ ĥęäđęř", + "repeat": "Ŗępęäŧ ƒőř", + "title": "Ŧįŧľę" + }, + "options": { + "height-expand": "Ēχpäʼnđ", + "height-min": "Mįʼn" + }, "row": { "collapse": "Cőľľäpşę řőŵ", "expand": "Ēχpäʼnđ řőŵ", @@ -1234,15 +1245,6 @@ } }, "row-options": { - "height": { - "hide-row-header": "Ħįđę řőŵ ĥęäđęř" - }, - "repeat": { - "variable": { - "title": "Ŗępęäŧ ƒőř" - } - }, - "title": "Ŗőŵ", "title-option": "Ŧįŧľę" } },