Dashboard: Ungroup action to remove row or tab (#103370)

* Dashboard: Ungroup action to remove row or tab

* Update
This commit is contained in:
Torkel Ödegaard
2025-04-04 12:07:58 +02:00
committed by GitHub
parent 5aa358c481
commit 49ecd83b87
6 changed files with 87 additions and 21 deletions

View File

@ -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,

View File

@ -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<RowsLayoutManagerState> 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);
}

View File

@ -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<TabsLayoutManagerState> 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();

View File

@ -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 (
<div className={cx(styles.addAction, 'dashboard-canvas-add-button')}>
@ -52,22 +55,69 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
<Trans i18nKey="dashboard.canvas-actions.group-panels">Group panels</Trans>
</Button>
</Dropdown>
{hasCopiedPanel && layoutManager.pastePanel && (
<Button
variant="primary"
fill="text"
icon="layers"
onClick={() => {
layoutManager.pastePanel?.();
}}
>
<Trans i18nKey="dashboard.canvas-actions.paste-panel">Paste panel</Trans>
</Button>
)}
{}
{renderUngroupAction(layoutManager)}
</div>
);
}
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 <UngroupButtonTabs parentLayout={parentLayout} onClick={onUngroup} />;
}
if (parentLayout instanceof RowsLayoutManager) {
return <UngroupButtonRows parentLayout={parentLayout} onClick={onUngroup} />;
}
return null;
}
interface UngroupButtonProps<T> {
parentLayout: T;
onClick: () => void;
}
function UngroupButtonTabs({ parentLayout, onClick }: UngroupButtonProps<TabsLayoutManager>) {
const { tabs } = parentLayout.useState();
if (tabs.length > 1) {
return null;
}
return (
<Button variant="primary" fill="text" icon="layers-slash" onClick={onClick}>
<Trans i18nKey="dashboard.canvas-actions.un-group-panels">Ungroup</Trans>
</Button>
);
}
function UngroupButtonRows({ parentLayout, onClick }: UngroupButtonProps<RowsLayoutManager>) {
const { rows } = parentLayout.useState();
if (rows.length > 1) {
return null;
}
return (
<Button variant="primary" fill="text" icon="layers-slash" onClick={onClick}>
<Trans i18nKey="dashboard.canvas-actions.un-group-panels">Ungroup</Trans>
</Button>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
addAction: css({
position: 'absolute',

View File

@ -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);
}
}

View File

@ -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": {