diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index a83da646ca8..737629562fc 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -16,6 +16,7 @@ import { } from '../../utils/utils'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { clearClipboard, getAutoGridItemFromClipboard } from '../layouts-shared/paste'; +import { DashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -38,10 +39,7 @@ export const AUTO_GRID_DEFAULT_MAX_COLUMN_COUNT = 3; export const AUTO_GRID_DEFAULT_COLUMN_WIDTH = 'standard'; export const AUTO_GRID_DEFAULT_ROW_HEIGHT = 'standard'; -export class AutoGridLayoutManager - extends SceneObjectBase - implements DashboardLayoutManager -{ +export class AutoGridLayoutManager extends SceneObjectBase implements DashboardLayoutGrid { public static Component = AutoGridLayoutManagerRenderer; public readonly isDashboardLayoutManager = true; @@ -200,12 +198,11 @@ export class AutoGridLayoutManager }); } - public merge(other: DashboardLayoutManager) { - if (!(other instanceof AutoGridLayoutManager)) { - throw new Error('Cannot merge non-auto grid layout'); - } - - const sourceLayout = other.state.layout; + public mergeGrid(other: DashboardLayoutGrid) { + const sourceLayout = + other instanceof AutoGridLayoutManager + ? other.state.layout + : AutoGridLayoutManager.createFromLayout(other).state.layout; const movedChildren = [...sourceLayout.state.children]; // Remove from source and append to destination diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 8c62ed47db0..294dc4230f7 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -48,6 +48,7 @@ import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions'; import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-shared/paste'; import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles'; import { getIsLazy } from '../layouts-shared/utils'; +import { DashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -62,7 +63,7 @@ interface DefaultGridLayoutManagerState extends SceneObjectState { export class DefaultGridLayoutManager extends SceneObjectBase - implements DashboardLayoutManager + implements DashboardLayoutGrid { public static Component = DefaultGridLayoutManagerRenderer; @@ -93,11 +94,7 @@ export class DefaultGridLayoutManager this.addActivationHandler(() => this._activationHandler()); } - public merge(other: DashboardLayoutManager) { - if (!(other instanceof DefaultGridLayoutManager)) { - throw new Error('Cannot merge non-default grid layout'); - } - + public mergeGrid(other: DashboardLayoutGrid) { let offset = 0; for (const child of this.state.grid.state.children) { const newOffset = (child.state.y ?? 0) + (child.state.height ?? 0); @@ -106,7 +103,10 @@ export class DefaultGridLayoutManager } } - const sourceGrid = other.state.grid; + const sourceGrid = + other instanceof DefaultGridLayoutManager + ? other.state.grid + : DefaultGridLayoutManager.createFromLayout(other).state.grid; const movedChildren = [...sourceGrid.state.children]; for (const child of movedChildren) { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.test.tsx index dbadd5def5e..ba8063aeb26 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.test.tsx @@ -117,12 +117,14 @@ describe('RowsLayoutManager', () => { expect(manager.state.rows).toContain(row2); }); - it('should call ungroupLayout when removing the last row', () => { + it('should not call ungroupLayout when removing the last row', () => { const manager = new RowsLayoutManager({ rows: [] }); const row = manager.addNewRow(new RowItem({ title: 'Only Row' })); expect(manager.state.rows).toHaveLength(1); manager.removeRow(row); - expect(ungroupLayoutCalled).toBe(true); + // This behavior was changed in the PR https://github.com/grafana/grafana/pull/112575 + // The delete row button should have one consistent behavior, no matter if it's the last row or not. + expect(ungroupLayoutCalled).toBe(false); }); }); }); 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 3a2cd8df171..49c3e0fd0a9 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -22,6 +22,8 @@ import { findAllGridTypes } from '../layouts-shared/findAllGridTypes'; import { getRowFromClipboard } from '../layouts-shared/paste'; import { showConvertMixedGridsModal, showUngroupConfirmation } from '../layouts-shared/ungroupConfirmation'; import { generateUniqueTitle, ungroupLayout, GridLayoutType, mapIdToGridLayoutType } from '../layouts-shared/utils'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; +import { DashboardLayoutGroup, isDashboardLayoutGroup } from '../types/DashboardLayoutGroup'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -33,7 +35,7 @@ interface RowsLayoutManagerState extends SceneObjectState { rows: RowItem[]; } -export class RowsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { +export class RowsLayoutManager extends SceneObjectBase implements DashboardLayoutGroup { public static Component = RowLayoutManagerRenderer; public readonly isDashboardLayoutManager = true; @@ -134,7 +136,7 @@ export class RowsLayoutManager extends SceneObjectBase i return outlineChildren; } - public convertAllRowsLayouts(gridLayoutType: GridLayoutType) { + public convertAllGridLayouts(gridLayoutType: GridLayoutType) { for (const row of this.state.rows) { switch (gridLayoutType) { case GridLayoutType.AutoGridLayout: @@ -189,7 +191,7 @@ export class RowsLayoutManager extends SceneObjectBase i description: t('dashboard.rows-layout.edit.ungroup-rows', 'Ungroup rows'), source: scene, perform: () => { - this._ungroupRows(gridLayoutType); + this.ungroup(gridLayoutType); }, undo: () => { parent.switchLayout(previousLayout); @@ -197,15 +199,15 @@ export class RowsLayoutManager extends SceneObjectBase i }); } - private _ungroupRows(gridLayoutType: GridLayoutType) { + public ungroup(gridLayoutType: GridLayoutType) { const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout); if (hasNonGridLayout) { for (const row of this.state.rows) { const layout = row.getLayout(); if (!layout.descriptor.isGridLayout) { - if (layout instanceof RowsLayoutManager) { - layout._ungroupRows(gridLayoutType); + if (isDashboardLayoutGroup(layout)) { + layout.ungroup(gridLayoutType); } else { throw new Error(`Ungrouping not supported for layout type: ${layout.descriptor.name}`); } @@ -213,7 +215,7 @@ export class RowsLayoutManager extends SceneObjectBase i } } - this.convertAllRowsLayouts(gridLayoutType); + this.convertAllGridLayouts(gridLayoutType); const firstRow = this.state.rows[0]; const firstRowLayout = firstRow.getLayout(); @@ -221,8 +223,8 @@ export class RowsLayoutManager extends SceneObjectBase i for (const row of otherRows) { const layout = row.getLayout(); - if (firstRowLayout.merge) { - firstRowLayout.merge(layout); + if (isDashboardLayoutGrid(firstRowLayout) && isDashboardLayoutGrid(layout)) { + firstRowLayout.mergeGrid(layout); } else { throw new Error(`Layout type ${firstRowLayout.descriptor.name} does not support merging`); } @@ -230,15 +232,10 @@ export class RowsLayoutManager extends SceneObjectBase i this.setState({ rows: [firstRow] }); this.removeRow(firstRow, true); + ungroupLayout(this, firstRow.state.layout, true); } public removeRow(row: RowItem, skipUndo?: boolean) { - // When removing last row replace ourselves with the inner row layout - if (this.shouldUngroup()) { - ungroupLayout(this, row.state.layout, skipUndo ?? false); - return; - } - const indexOfRowToRemove = this.state.rows.findIndex((r) => r === row); const perform = () => this.setState({ rows: this.state.rows.filter((r) => r !== row) }); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx index 85310dc65bf..ce58ed2c5ef 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx @@ -53,15 +53,6 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps - )} + )} 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 33229cc8964..10933b9c0b2 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -21,6 +21,8 @@ import { findAllGridTypes } from '../layouts-shared/findAllGridTypes'; import { getTabFromClipboard } from '../layouts-shared/paste'; import { showConvertMixedGridsModal, showUngroupConfirmation } from '../layouts-shared/ungroupConfirmation'; import { generateUniqueTitle, ungroupLayout, GridLayoutType, mapIdToGridLayoutType } from '../layouts-shared/utils'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; +import { DashboardLayoutGroup, isDashboardLayoutGroup } from '../types/DashboardLayoutGroup'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -33,7 +35,7 @@ interface TabsLayoutManagerState extends SceneObjectState { currentTabSlug?: string; } -export class TabsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { +export class TabsLayoutManager extends SceneObjectBase implements DashboardLayoutGroup { public static Component = TabsLayoutManagerRenderer; public readonly isDashboardLayoutManager = true; @@ -71,21 +73,6 @@ export class TabsLayoutManager extends SceneObjectBase i throw new Error('Method not implemented.'); } - public merge(other: DashboardLayoutManager) { - if (!(other instanceof TabsLayoutManager)) { - throw new Error('Cannot merge non-tabs layout'); - } - - // Merge all tabs from the other layout into this one - const otherTabs = other.state.tabs; - const mergedTabs = [...this.state.tabs, ...otherTabs]; - - // Clear parent from merged tabs to avoid conflicts - otherTabs.forEach((tab) => tab.clearParent()); - - this.setState({ tabs: mergedTabs }); - } - public duplicateTab(tab: TabItem) { const newTab = tab.duplicate(); this.addNewTab(newTab); @@ -223,7 +210,7 @@ export class TabsLayoutManager extends SceneObjectBase i return this.state.tabs.length === 1; } - public convertAllTabsLayouts(gridLayoutType: GridLayoutType) { + public convertAllGridLayouts(gridLayoutType: GridLayoutType) { for (const tab of this.state.tabs) { switch (gridLayoutType) { case GridLayoutType.AutoGridLayout: @@ -278,7 +265,7 @@ export class TabsLayoutManager extends SceneObjectBase i description: t('dashboard.tabs-layout.edit.ungroup-tabs', 'Ungroup tabs'), source: scene, perform: () => { - this._ungroupTabs(gridLayoutType); + this.ungroup(gridLayoutType); }, undo: () => { parent.switchLayout(previousLayout); @@ -286,17 +273,15 @@ export class TabsLayoutManager extends SceneObjectBase i }); } - private _ungroupTabs(gridLayoutType: GridLayoutType) { + public ungroup(gridLayoutType: GridLayoutType) { const hasNonGridLayout = this.state.tabs.some((tab) => !tab.getLayout().descriptor.isGridLayout); if (hasNonGridLayout) { for (const tab of this.state.tabs) { const layout = tab.getLayout(); if (!layout.descriptor.isGridLayout) { - if (layout instanceof TabsLayoutManager) { - layout._ungroupTabs(gridLayoutType); - } else if (layout instanceof RowsLayoutManager) { - layout.ungroupRows(); + if (isDashboardLayoutGroup(layout)) { + layout.ungroup(gridLayoutType); } else { throw new Error(`Ungrouping not supported for layout type: ${layout.descriptor.name}`); } @@ -304,7 +289,7 @@ export class TabsLayoutManager extends SceneObjectBase i } } - this.convertAllTabsLayouts(gridLayoutType); + this.convertAllGridLayouts(gridLayoutType); const firstTab = this.state.tabs[0]; const firstTabLayout = firstTab.getLayout(); @@ -312,24 +297,18 @@ export class TabsLayoutManager extends SceneObjectBase i for (const tab of otherTabs) { const layout = tab.getLayout(); - if (firstTabLayout.merge) { - firstTabLayout.merge(layout); + if (isDashboardLayoutGrid(firstTabLayout) && isDashboardLayoutGrid(layout)) { + firstTabLayout.mergeGrid(layout); } else { throw new Error(`Layout type ${firstTabLayout.descriptor.name} does not support merging`); } } this.setState({ tabs: [firstTab] }); - this.removeTab(firstTab, true); + ungroupLayout(this, firstTab.state.layout, true); } public removeTab(tabToRemove: TabItem, skipUndo?: boolean) { - // When removing last tab replace ourselves with the inner tab layout - if (this.shouldUngroup()) { - ungroupLayout(this, tabToRemove.state.layout, skipUndo ?? false); - return; - } - const tabIndex = this.state.tabs.findIndex((t) => t === tabToRemove); const perform = () => { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts new file mode 100644 index 00000000000..3f6bcb2bcc6 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts @@ -0,0 +1,12 @@ +import { DashboardLayoutManager } from './DashboardLayoutManager'; + +export interface DashboardLayoutGrid extends DashboardLayoutManager { + /** + * Merge the layout with another layout + */ + mergeGrid(other: DashboardLayoutGrid): void; +} + +export function isDashboardLayoutGrid(obj: DashboardLayoutManager): obj is DashboardLayoutGrid { + return 'mergeGrid' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGroup.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGroup.ts new file mode 100644 index 00000000000..034eb8642e2 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGroup.ts @@ -0,0 +1,21 @@ +import { GridLayoutType } from '../layouts-shared/utils'; + +import { DashboardLayoutManager } from './DashboardLayoutManager'; + +export interface DashboardLayoutGroup extends DashboardLayoutManager { + /** + * Ungroup the group + * @param gridLayoutType + */ + ungroup(gridLayoutType: GridLayoutType): void; + + /** + * Convert all layouts to the given grid layout type + * @param gridLayoutType + */ + convertAllGridLayouts(gridLayoutType: GridLayoutType): void; +} + +export function isDashboardLayoutGroup(obj: DashboardLayoutManager): obj is DashboardLayoutGroup { + return 'ungroup' in obj && 'convertAllGridLayouts' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 0db0463bc22..73a9d7329e0 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -86,11 +86,6 @@ export interface DashboardLayoutManager extends SceneObject { * Get children for outline */ getOutlineChildren(): SceneObject[]; - - /** - * Merge the layout with another layout - */ - merge?(other: DashboardLayoutManager): void; } export interface LayoutManagerSerializer {