mirror of
https://github.com/grafana/grafana.git
synced 2026-08-19 01:34:54 -05:00
Dynamic dashboards: Ungroup tabs (#112332)
* ungroup tabs * fix i18n * break out confirmation from utils * update tests * click convert in modal in e2e
This commit is contained in:
@@ -92,8 +92,8 @@ test.describe(
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
// Ungroup
|
||||
await ungroupPanels(dashboardPage, selectors);
|
||||
// Ungroup using the new ungroup rows button
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroupRows).click();
|
||||
|
||||
// Verify Row title is gone
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
|
||||
@@ -112,7 +112,7 @@ test.describe(
|
||||
).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('can add and remove several rows', async ({ dashboardPage, selectors, page }) => {
|
||||
test('can add multiple rows and ungroup them all at once', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(page, selectors, 'Add and remove rows');
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
@@ -171,6 +171,7 @@ test.describe(
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
// First test individual row deletion
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
|
||||
.locator('..')
|
||||
@@ -178,29 +179,38 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
|
||||
.locator('..')
|
||||
.click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
|
||||
|
||||
// Verify one row is deleted
|
||||
await expect(firstRow).toBeVisible();
|
||||
await expect(secondRow).toBeHidden();
|
||||
await expect(thirdRow).toBeVisible();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(4); // 3 from first row + 1 from third row
|
||||
|
||||
// Now test ungrouping all remaining rows at once
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroupRows).click();
|
||||
|
||||
// Handle the ConvertMixedGridsModal that appears when there are mixed grid types
|
||||
// The modal asks which grid type to convert to - we'll choose "Custom" (GridLayout)
|
||||
await page.getByRole('button', { name: 'Convert to Custom' }).click();
|
||||
|
||||
// Verify all remaining rows are gone and all panels are now in a single grid
|
||||
await expect(firstRow).toBeHidden();
|
||||
await expect(thirdRow).toBeHidden();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
).toHaveCount(4); // All 4 panels should be visible in the single grid
|
||||
|
||||
await saveDashboard(dashboardPage, selectors);
|
||||
await page.reload();
|
||||
|
||||
await expect(firstRow).toBeVisible();
|
||||
// Verify all rows are still gone after reload
|
||||
await expect(firstRow).toBeHidden();
|
||||
await expect(secondRow).toBeHidden();
|
||||
await expect(thirdRow).toBeHidden();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
).toHaveCount(4);
|
||||
});
|
||||
|
||||
test('can paste a copied row', async ({ dashboardPage, selectors, page }) => {
|
||||
@@ -473,7 +483,7 @@ test.describe(
|
||||
|
||||
// Ungroup
|
||||
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
|
||||
await ungroupPanels(dashboardPage, selectors); // ungroup rows
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroupRows).click(); // ungroup rows
|
||||
|
||||
// Verify tab and row titles is gone
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
|
||||
@@ -804,7 +814,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
// Ungroup
|
||||
await ungroupPanels(dashboardPage, selectors); // ungroup rows
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroupRows).click(); // ungroup rows
|
||||
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
|
||||
|
||||
// Verify tab and row titles is gone
|
||||
|
||||
@@ -33,6 +33,9 @@ export const versionedComponents = {
|
||||
ungroup: {
|
||||
'12.1.0': 'data-testid CanvasGridAddActions ungroup',
|
||||
},
|
||||
ungroupRows: {
|
||||
'12.1.0': 'data-testid CanvasGridAddActions ungroup-rows',
|
||||
},
|
||||
addRow: {
|
||||
'12.1.0': 'data-testid CanvasGridAddActions add-row',
|
||||
},
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowConfirmModalEvent, ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared';
|
||||
import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
|
||||
@@ -22,12 +20,12 @@ import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { findAllGridTypes } from '../layouts-shared/findAllGridTypes';
|
||||
import { getRowFromClipboard } from '../layouts-shared/paste';
|
||||
import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils';
|
||||
import { showConvertMixedGridsModal, showUngroupConfirmation } from '../layouts-shared/ungroupConfirmation';
|
||||
import { generateUniqueTitle, ungroupLayout, GridLayoutType, mapIdToGridLayoutType } from '../layouts-shared/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { isLayoutParent } from '../types/LayoutParent';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { ConvertMixedGridsModal } from './ConvertMixedGridsModal';
|
||||
import { RowItem } from './RowItem';
|
||||
import { RowLayoutManagerRenderer } from './RowsLayoutManagerRenderer';
|
||||
|
||||
@@ -35,22 +33,6 @@ interface RowsLayoutManagerState extends SceneObjectState {
|
||||
rows: RowItem[];
|
||||
}
|
||||
|
||||
enum GridLayoutType {
|
||||
AutoGridLayout = 'AutoGridLayout',
|
||||
GridLayout = 'GridLayout',
|
||||
}
|
||||
|
||||
function mapIdToGridLayoutType(id?: string): GridLayoutType | undefined {
|
||||
switch (id) {
|
||||
case GridLayoutType.AutoGridLayout:
|
||||
return GridLayoutType.AutoGridLayout;
|
||||
case GridLayoutType.GridLayout:
|
||||
return GridLayoutType.GridLayout;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> implements DashboardLayoutManager {
|
||||
public static Component = RowLayoutManagerRenderer;
|
||||
public readonly isDashboardLayoutManager = true;
|
||||
@@ -171,50 +153,25 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout);
|
||||
const gridTypes = new Set(findAllGridTypes(this));
|
||||
|
||||
if (hasNonGridLayout) {
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: t('dashboard.rows-layout.ungroup-nested-title', 'Ungroup nested groups?'),
|
||||
text: t('dashboard.rows-layout.ungroup-nested-text', 'This will ungroup all nested groups.'),
|
||||
yesText: t('dashboard.rows-layout.continue', 'Continue'),
|
||||
noText: t('dashboard.rows-layout.cancel', 'Cancel'),
|
||||
onConfirm: () => {
|
||||
if (gridTypes.size > 1) {
|
||||
requestAnimationFrame(() => {
|
||||
this._confirmConvertMixedGrids(gridTypes);
|
||||
});
|
||||
} else {
|
||||
this.wrapUngroupRowsInEdit(mapIdToGridLayoutType(gridTypes.values().next().value)!);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gridTypes.size > 1) {
|
||||
this._confirmConvertMixedGrids(gridTypes);
|
||||
return;
|
||||
} else {
|
||||
this.wrapUngroupRowsInEdit(mapIdToGridLayoutType(gridTypes.values().next().value)!);
|
||||
}
|
||||
showUngroupConfirmation({
|
||||
hasNonGridLayout,
|
||||
gridTypes,
|
||||
onConfirm: (gridLayoutType) => {
|
||||
this.wrapUngroupRowsInEdit(gridLayoutType);
|
||||
},
|
||||
onConvertMixedGrids: (availableIds) => {
|
||||
this._confirmConvertMixedGrids(availableIds);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _confirmConvertMixedGrids(availableIds: Set<string>) {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: ConvertMixedGridsModal,
|
||||
props: {
|
||||
availableIds,
|
||||
onSelect: (id: string) => {
|
||||
const selected = mapIdToGridLayoutType(id);
|
||||
if (selected) {
|
||||
this.wrapUngroupRowsInEdit(selected);
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
showConvertMixedGridsModal(availableIds, (id: string) => {
|
||||
const selected = mapIdToGridLayoutType(id);
|
||||
if (selected) {
|
||||
this.wrapUngroupRowsInEdit(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private wrapUngroupRowsInEdit(gridLayoutType: GridLayoutType) {
|
||||
|
||||
@@ -53,7 +53,13 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
|
||||
{dropProvided.placeholder}
|
||||
{isEditing && !isClone && (
|
||||
<div className="dashboard-canvas-add-button">
|
||||
<Button icon="layers-slash" variant="primary" fill="text" onClick={() => model.ungroupRows()}>
|
||||
<Button
|
||||
icon="layers-slash"
|
||||
variant="primary"
|
||||
fill="text"
|
||||
onClick={() => model.ungroupRows()}
|
||||
data-testid={selectors.components.CanvasGridAddActions.ungroupRows}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.ungroup-rows">Ungroup rows</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -13,11 +13,16 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa
|
||||
import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared';
|
||||
import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
import { RowItem } from '../layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { findAllGridTypes } from '../layouts-shared/findAllGridTypes';
|
||||
import { getTabFromClipboard } from '../layouts-shared/paste';
|
||||
import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils';
|
||||
import { showConvertMixedGridsModal, showUngroupConfirmation } from '../layouts-shared/ungroupConfirmation';
|
||||
import { generateUniqueTitle, ungroupLayout, GridLayoutType, mapIdToGridLayoutType } from '../layouts-shared/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { isLayoutParent } from '../types/LayoutParent';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
@@ -66,6 +71,21 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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);
|
||||
@@ -203,36 +223,144 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
return this.state.tabs.length === 1;
|
||||
}
|
||||
|
||||
public removeTab(tabToRemove: TabItem) {
|
||||
public convertAllTabsLayouts(gridLayoutType: GridLayoutType) {
|
||||
for (const tab of this.state.tabs) {
|
||||
switch (gridLayoutType) {
|
||||
case GridLayoutType.AutoGridLayout:
|
||||
if (!(tab.getLayout() instanceof AutoGridLayoutManager)) {
|
||||
tab.switchLayout(AutoGridLayoutManager.createFromLayout(tab.getLayout()));
|
||||
}
|
||||
break;
|
||||
case GridLayoutType.GridLayout:
|
||||
if (!(tab.getLayout() instanceof DefaultGridLayoutManager)) {
|
||||
tab.switchLayout(DefaultGridLayoutManager.createFromLayout(tab.getLayout()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ungroupTabs() {
|
||||
const hasNonGridLayout = this.state.tabs.some((tab) => !tab.getLayout().descriptor.isGridLayout);
|
||||
const gridTypes = new Set(findAllGridTypes(this));
|
||||
|
||||
showUngroupConfirmation({
|
||||
hasNonGridLayout,
|
||||
gridTypes,
|
||||
onConfirm: (gridLayoutType) => {
|
||||
this.wrapUngroupTabsInEdit(gridLayoutType);
|
||||
},
|
||||
onConvertMixedGrids: (availableIds) => {
|
||||
this._confirmConvertMixedGrids(availableIds);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _confirmConvertMixedGrids(availableIds: Set<string>) {
|
||||
showConvertMixedGridsModal(availableIds, (id: string) => {
|
||||
const selected = mapIdToGridLayoutType(id);
|
||||
if (selected) {
|
||||
this.wrapUngroupTabsInEdit(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private wrapUngroupTabsInEdit(gridLayoutType: GridLayoutType) {
|
||||
const parent = this.parent;
|
||||
if (!parent || !isLayoutParent(parent)) {
|
||||
throw new Error('Ungroup tabs failed: parent is not a layout container');
|
||||
}
|
||||
|
||||
const previousLayout = this.clone({});
|
||||
const scene = getDashboardSceneFor(this);
|
||||
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.tabs-layout.edit.ungroup-tabs', 'Ungroup tabs'),
|
||||
source: scene,
|
||||
perform: () => {
|
||||
this._ungroupTabs(gridLayoutType);
|
||||
},
|
||||
undo: () => {
|
||||
parent.switchLayout(previousLayout);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _ungroupTabs(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();
|
||||
} else {
|
||||
throw new Error(`Ungrouping not supported for layout type: ${layout.descriptor.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.convertAllTabsLayouts(gridLayoutType);
|
||||
|
||||
const firstTab = this.state.tabs[0];
|
||||
const firstTabLayout = firstTab.getLayout();
|
||||
const otherTabs = this.state.tabs.slice(1);
|
||||
|
||||
for (const tab of otherTabs) {
|
||||
const layout = tab.getLayout();
|
||||
if (firstTabLayout.merge) {
|
||||
firstTabLayout.merge(layout);
|
||||
} else {
|
||||
throw new Error(`Layout type ${firstTabLayout.descriptor.name} does not support merging`);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ tabs: [firstTab] });
|
||||
this.removeTab(firstTab, 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);
|
||||
ungroupLayout(this, tabToRemove.state.layout, skipUndo ?? false);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.state.tabs.findIndex((t) => t === tabToRemove);
|
||||
|
||||
dashboardEditActions.removeElement({
|
||||
removedObject: tabToRemove,
|
||||
source: this,
|
||||
perform: () => {
|
||||
const tabs = this.state.tabs;
|
||||
const tabIndex = tabs.findIndex((t) => t === tabToRemove);
|
||||
const newCurrentTabIndex = tabIndex > 0 ? tabIndex - 1 : 0;
|
||||
const perform = () => {
|
||||
const tabs = this.state.tabs;
|
||||
const tabIndex = tabs.findIndex((t) => t === tabToRemove);
|
||||
const newCurrentTabIndex = tabIndex > 0 ? tabIndex - 1 : 0;
|
||||
|
||||
const newTabsState = tabs.filter((t) => t !== tabToRemove);
|
||||
const newTabsState = tabs.filter((t) => t !== tabToRemove);
|
||||
|
||||
this.setState({
|
||||
tabs: newTabsState,
|
||||
currentTabSlug: newTabsState[newCurrentTabIndex]?.getSlug(),
|
||||
});
|
||||
},
|
||||
undo: () => {
|
||||
const tabs = [...this.state.tabs];
|
||||
tabs.splice(tabIndex, 0, tabToRemove);
|
||||
this.setState({ tabs, currentTabSlug: tabToRemove.getSlug() });
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
tabs: newTabsState,
|
||||
currentTabSlug: newTabsState[newCurrentTabIndex]?.getSlug(),
|
||||
});
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
const tabs = [...this.state.tabs];
|
||||
tabs.splice(tabIndex, 0, tabToRemove);
|
||||
this.setState({ tabs, currentTabSlug: tabToRemove.getSlug() });
|
||||
};
|
||||
|
||||
if (skipUndo) {
|
||||
perform();
|
||||
} else {
|
||||
dashboardEditActions.removeElement({
|
||||
removedObject: tabToRemove,
|
||||
source: this,
|
||||
perform,
|
||||
undo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public moveTab(fromIndex: number, toIndex: number) {
|
||||
|
||||
@@ -92,6 +92,9 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
<Trans i18nKey="dashboard.canvas-actions.paste-tab">Paste tab</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<Button icon="layers-slash" variant="primary" fill="text" onClick={() => model.ungroupTabs()}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.ungroup-tabs">Ungroup tabs</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager, isDashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
@@ -155,10 +154,6 @@ function renderUngroupAction(layoutManager: DashboardLayoutManager) {
|
||||
return <UngroupButtonTabs parentLayout={parentLayout} onClick={onUngroup} />;
|
||||
}
|
||||
|
||||
if (parentLayout instanceof RowsLayoutManager) {
|
||||
return <UngroupButtonRows parentLayout={parentLayout} onClick={onUngroup} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -187,26 +182,6 @@ function UngroupButtonTabs({ parentLayout, onClick }: UngroupButtonProps<TabsLay
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
data-testid={selectors.components.CanvasGridAddActions.ungroup}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.un-group-panels">Ungroup</Trans>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
addAction: css({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowConfirmModalEvent, ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { ConvertMixedGridsModal } from '../layout-rows/ConvertMixedGridsModal';
|
||||
|
||||
import { GridLayoutType, mapIdToGridLayoutType } from './utils';
|
||||
|
||||
export interface UngroupConfirmationOptions {
|
||||
hasNonGridLayout: boolean;
|
||||
gridTypes: Set<string>;
|
||||
onConfirm: (gridLayoutType: GridLayoutType) => void;
|
||||
onConvertMixedGrids: (availableIds: Set<string>) => void;
|
||||
}
|
||||
|
||||
export function showUngroupConfirmation({
|
||||
hasNonGridLayout,
|
||||
gridTypes,
|
||||
onConfirm,
|
||||
onConvertMixedGrids,
|
||||
}: UngroupConfirmationOptions) {
|
||||
if (hasNonGridLayout) {
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: t('dashboard.layout.ungroup-nested-title', 'Ungroup nested groups?'),
|
||||
text: t('dashboard.layout.ungroup-nested-text', 'This will ungroup all nested groups.'),
|
||||
yesText: t('dashboard.layout.continue', 'Continue'),
|
||||
noText: t('dashboard.layout.cancel', 'Cancel'),
|
||||
onConfirm: () => {
|
||||
if (gridTypes.size > 1) {
|
||||
requestAnimationFrame(() => {
|
||||
onConvertMixedGrids(gridTypes);
|
||||
});
|
||||
} else {
|
||||
const gridLayoutType = mapIdToGridLayoutType(gridTypes.values().next().value);
|
||||
if (gridLayoutType) {
|
||||
onConfirm(gridLayoutType);
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gridTypes.size > 1) {
|
||||
onConvertMixedGrids(gridTypes);
|
||||
return;
|
||||
} else {
|
||||
const gridLayoutType = mapIdToGridLayoutType(gridTypes.values().next().value);
|
||||
if (gridLayoutType) {
|
||||
onConfirm(gridLayoutType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showConvertMixedGridsModal(availableIds: Set<string>, onSelect: (id: string) => void) {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: ConvertMixedGridsModal,
|
||||
props: {
|
||||
availableIds,
|
||||
onSelect,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -81,3 +81,19 @@ export function getIsLazy(preload: boolean | undefined): boolean {
|
||||
// We don't want to lazy load panels in the case of image renderer
|
||||
return !(preload || (contextSrv.user && contextSrv.user.authenticatedBy === 'render'));
|
||||
}
|
||||
|
||||
export enum GridLayoutType {
|
||||
AutoGridLayout = 'AutoGridLayout',
|
||||
GridLayout = 'GridLayout',
|
||||
}
|
||||
|
||||
export function mapIdToGridLayoutType(id?: string): GridLayoutType | undefined {
|
||||
switch (id) {
|
||||
case GridLayoutType.AutoGridLayout:
|
||||
return GridLayoutType.AutoGridLayout;
|
||||
case GridLayoutType.GridLayout:
|
||||
return GridLayoutType.GridLayout;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4535,7 +4535,8 @@
|
||||
"paste-row": "Paste row",
|
||||
"paste-tab": "Paste tab",
|
||||
"un-group-panels": "Ungroup",
|
||||
"ungroup-rows": "Ungroup rows"
|
||||
"ungroup-rows": "Ungroup rows",
|
||||
"ungroup-tabs": "Ungroup tabs"
|
||||
},
|
||||
"conditional-rendering": {
|
||||
"conditions": {
|
||||
@@ -5046,6 +5047,7 @@
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"cancel": "Cancel",
|
||||
"common": {
|
||||
"copy": "Copy",
|
||||
"copy-or-duplicate": "Copy or Duplicate",
|
||||
@@ -5055,7 +5057,10 @@
|
||||
"group-layout-disabled": "No groups exists on this level",
|
||||
"panel-layout": "Panel layout",
|
||||
"panel-layout-disabled": "Select a row or tab to change panel layout options"
|
||||
}
|
||||
},
|
||||
"continue": "Continue",
|
||||
"ungroup-nested-text": "This will ungroup all nested groups.",
|
||||
"ungroup-nested-title": "Ungroup nested groups?"
|
||||
},
|
||||
"legacy-dashboard-api": {
|
||||
"fetch-error": {
|
||||
@@ -5205,7 +5210,6 @@
|
||||
},
|
||||
"rows-layout": {
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"convert-to": "Convert to {{name}}",
|
||||
"delete-row-text": "Deleting this row will also remove all panels. Are you sure you want to continue?",
|
||||
"delete-row-title": "Delete row?",
|
||||
@@ -5245,9 +5249,7 @@
|
||||
"title-not-unique": "This title is not unique"
|
||||
},
|
||||
"ungroup-convert-text": "All grids must be converted to the same type and positions will be lost.",
|
||||
"ungroup-convert-title": "Convert mixed grids?",
|
||||
"ungroup-nested-text": "This will ungroup all nested groups.",
|
||||
"ungroup-nested-title": "Ungroup nested groups?"
|
||||
"ungroup-convert-title": "Convert mixed grids?"
|
||||
},
|
||||
"save-dashboard": {
|
||||
"message-dashboard-saved": "Dashboard saved"
|
||||
@@ -5337,6 +5339,9 @@
|
||||
"delete-tab-title": "Delete tab?",
|
||||
"delete-tab-yes": "Delete",
|
||||
"description": "Organize panels into horizontal tabs",
|
||||
"edit": {
|
||||
"ungroup-tabs": "Ungroup tabs"
|
||||
},
|
||||
"name": "Tabs",
|
||||
"tab": {
|
||||
"new": "New tab",
|
||||
|
||||
Reference in New Issue
Block a user