diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index a9fb6cf08d5..3570094ff31 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -172,6 +172,7 @@ export const availableIconsIndex = { 'layer-group': true, 'layers-alt': true, layers: true, + 'layers-slash': true, 'legend-hide': true, 'legend-show': true, 'library-panel': true, 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 b2da3a617f3..fb4240520af 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -16,7 +16,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { getRowFromClipboard } from '../layouts-shared/paste'; -import { generateUniqueTitle } from '../layouts-shared/utils'; +import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -134,8 +134,14 @@ export class RowsLayoutManager extends SceneObjectBase i } public removeRow(row: RowItem) { + // When removing last row replace ourselves with the inner row layout + if (this.state.rows.length === 1) { + ungroupLayout(this, row.state.layout); + return; + } + const rows = this.state.rows.filter((r) => r !== row); - this.setState({ rows: rows.length === 0 ? [new RowItem()] : rows }); + this.setState({ rows }); this.publishEvent(new ObjectRemovedFromCanvasEvent(row), true); } 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 039a889ec21..b73dc7eda73 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -18,7 +18,7 @@ import { getDashboardSceneFor } from '../../utils/utils'; import { RowItem } from '../layout-rows/RowItem'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { getTabFromClipboard } from '../layouts-shared/paste'; -import { generateUniqueTitle } from '../layouts-shared/utils'; +import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -152,9 +152,9 @@ export class TabsLayoutManager extends SceneObjectBase i } public removeTab(tabToRemove: TabItem) { - // Do not allow removing last tab (for now) + // When removing last tab replace ourselves with the inner tab layout if (this.state.tabs.length === 1) { - return; + ungroupLayout(this, tabToRemove.state.layout); } const currentTab = this.getCurrentTab(); diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx index 571a7c06ca0..cc70eb236ce 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx @@ -4,11 +4,15 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { getDefaultVizPanel } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { addNewRowTo, addNewTabTo } from './addNew'; -import { useClipboardState } from './useClipboardState'; +import { ungroupLayout } from './utils'; export interface Props { layoutManager: DashboardLayoutManager; @@ -16,7 +20,6 @@ export interface Props { export function CanvasGridAddActions({ layoutManager }: Props) { const styles = useStyles2(getStyles); - const { hasCopiedPanel } = useClipboardState(); return (
@@ -52,22 +55,69 @@ export function CanvasGridAddActions({ layoutManager }: Props) { Group panels - {hasCopiedPanel && layoutManager.pastePanel && ( - - )} + {} + {renderUngroupAction(layoutManager)}
); } +function renderUngroupAction(layoutManager: DashboardLayoutManager) { + const parent = layoutManager.parent; + + if (parent instanceof DashboardScene) { + return null; + } + + const parentLayout = dashboardSceneGraph.getLayoutManagerFor(layoutManager.parent!); + + const onUngroup = () => { + ungroupLayout(parentLayout, layoutManager); + }; + + if (parentLayout instanceof TabsLayoutManager) { + return ; + } + + if (parentLayout instanceof RowsLayoutManager) { + return ; + } + + return null; +} + +interface UngroupButtonProps { + parentLayout: T; + onClick: () => void; +} + +function UngroupButtonTabs({ parentLayout, onClick }: UngroupButtonProps) { + const { tabs } = parentLayout.useState(); + + if (tabs.length > 1) { + return null; + } + + return ( + + ); +} + +function UngroupButtonRows({ parentLayout, onClick }: UngroupButtonProps) { + const { rows } = parentLayout.useState(); + + if (rows.length > 1) { + return null; + } + + return ( + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ addAction: css({ position: 'absolute', diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts index 3d4b850d568..41402564081 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts @@ -3,6 +3,7 @@ import { useEffect, useRef } from 'react'; import { SceneObject } from '@grafana/scenes'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { isLayoutParent } from '../types/LayoutParent'; export function findParentLayout(sceneObject: SceneObject): DashboardLayoutManager | null { let parent = sceneObject.parent; @@ -66,3 +67,11 @@ export function generateUniqueTitle(title: string | undefined, existingTitles: S return baseTitle; } + +export function ungroupLayout(layout: DashboardLayoutManager, innerLayout: DashboardLayoutManager) { + const layoutParent = layout.parent!; + if (isLayoutParent(layoutParent)) { + innerLayout.clearParent(); + layoutParent.switchLayout(innerLayout); + } +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index fcd059380fc..10b1c45a9e0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1508,9 +1508,9 @@ "group-panels": "Group panels", "new-row": "New row", "new-tab": "New tab", - "paste-panel": "Paste panel", "paste-row": "Paste row", - "paste-tab": "Paste tab" + "paste-tab": "Paste tab", + "un-group-panels": "Ungroup" }, "conditional-rendering": { "conditions": {