diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx
index d363a95ddf0..a8c2cbb6ec0 100644
--- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx
+++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx
@@ -7,10 +7,13 @@ import { SceneGridLayout, VizPanel, SceneVariableSet } from '@grafana/scenes';
import { activateFullSceneTree } from '../../utils/test-utils';
import { DashboardScene } from '../DashboardScene';
+import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
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 { LayoutParent } from '../types/LayoutParent';
import { DashboardLayoutSelector } from './DashboardLayoutSelector';
@@ -40,6 +43,27 @@ describe('DashboardLayoutSelector', () => {
await user.click(confirmButton);
expect(switchLayoutMock).toHaveBeenCalled();
});
+
+ it('should disable tabs option when a row contains tabs layout and show correct message', async () => {
+ const scene = buildTestSceneWithNestedTabs();
+ const layoutManager = scene.state.body;
+
+ render();
+
+ const tabsOption = screen.getByLabelText('layout-selection-option-Tabs');
+ expect(tabsOption).toBeDisabled();
+ expect(screen.getByTitle('Cannot change to tabs because a row already contains tabs')).toBeInTheDocument();
+ });
+
+ it('should not disable tabs option when rows do not contain tabs', async () => {
+ const scene = buildTestScene();
+ const layoutManager = scene.state.body;
+
+ render();
+
+ const tabsOption = screen.getByLabelText('layout-selection-option-Tabs');
+ expect(tabsOption).not.toBeDisabled();
+ });
});
const buildTestScene = () => {
@@ -70,3 +94,43 @@ const buildTestScene = () => {
activateFullSceneTree(scene);
return scene;
};
+
+const buildTestSceneWithNestedTabs = () => {
+ const scene = new DashboardScene({
+ title: 'testScene',
+ editable: true,
+ $variables: new SceneVariableSet({
+ variables: [],
+ }),
+ body: new RowsLayoutManager({
+ rows: [
+ new RowItem({
+ title: 'Row 1',
+ layout: new DefaultGridLayoutManager({
+ grid: new SceneGridLayout({
+ children: [
+ new DashboardGridItem({
+ body: new VizPanel({ key: 'panel-1', pluginId: 'text' }),
+ }),
+ ],
+ }),
+ }),
+ }),
+ new RowItem({
+ title: 'Row with Tabs',
+ layout: new TabsLayoutManager({
+ tabs: [
+ new TabItem({
+ title: 'Tab 1',
+ layout: AutoGridLayoutManager.createEmpty(),
+ }),
+ ],
+ }),
+ }),
+ ],
+ }),
+ });
+
+ activateFullSceneTree(scene);
+ return scene;
+};
diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx
index b32555b32f8..ee902d195ad 100644
--- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx
+++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx
@@ -11,6 +11,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { isLayoutParent } from '../types/LayoutParent';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
+import { containsTabsLayout } from './findAllGridTypes';
import { layoutRegistry } from './layoutRegistry';
export interface Props {
@@ -22,19 +23,26 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout);
const [newLayout, setNewLayout] = useState();
- const disableTabs = useMemo(() => {
+ const disableTabsReason = useMemo(() => {
if (config.featureToggles.unlimitedLayoutsNesting) {
- return false;
+ return undefined;
}
+
+ // Check parent hierarchy
let parent = layoutManager.parent;
while (parent) {
if (parent instanceof TabsLayoutManager) {
- return true;
+ return 'parent';
}
parent = parent.parent;
}
- return false;
+ // Check child hierarchy
+ if (containsTabsLayout(layoutManager)) {
+ return 'child';
+ }
+
+ return undefined;
}, [layoutManager]);
const onChangeLayout = useCallback((newLayout: LayoutRegistryItem) => setNewLayout(newLayout), []);
@@ -59,8 +67,15 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
const radioOptions = options.map((opt) => {
let description = opt.description;
- if (disableTabs && opt.id === TabsLayoutManager.descriptor.id) {
- description = t('dashboard.canvas-actions.disabled-nested-tabs', 'Tabs cannot be nested inside other tabs');
+ if (disableTabsReason && opt.id === TabsLayoutManager.descriptor.id) {
+ if (disableTabsReason === 'parent') {
+ description = t('dashboard.canvas-actions.disabled-nested-tabs', 'Tabs cannot be nested inside other tabs');
+ } else {
+ description = t(
+ 'dashboard.canvas-actions.disabled-child-contains-tabs',
+ 'Cannot change to tabs because a row already contains tabs'
+ );
+ }
disabledOptions.push(opt);
}
diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.test.ts b/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.test.ts
new file mode 100644
index 00000000000..b2c925fb26e
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.test.ts
@@ -0,0 +1,93 @@
+import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
+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 { containsTabsLayout, findAllGridTypes } from './findAllGridTypes';
+
+describe('findAllGridTypes', () => {
+ it('should return grid type for a grid layout', () => {
+ const layout = AutoGridLayoutManager.createEmpty();
+ expect(findAllGridTypes(layout)).toEqual([AutoGridLayoutManager.descriptor.id]);
+ });
+
+ it('should return grid types from tabs', () => {
+ const layout = new TabsLayoutManager({
+ tabs: [
+ new TabItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ new TabItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ ],
+ });
+ expect(findAllGridTypes(layout)).toEqual([
+ AutoGridLayoutManager.descriptor.id,
+ AutoGridLayoutManager.descriptor.id,
+ ]);
+ });
+
+ it('should return grid types from rows', () => {
+ const layout = new RowsLayoutManager({
+ rows: [
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ ],
+ });
+ expect(findAllGridTypes(layout)).toEqual([
+ AutoGridLayoutManager.descriptor.id,
+ AutoGridLayoutManager.descriptor.id,
+ ]);
+ });
+});
+
+describe('containsTabsLayout', () => {
+ it('should return true when layout is TabsLayoutManager', () => {
+ const layout = new TabsLayoutManager({
+ tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
+ });
+ expect(containsTabsLayout(layout)).toBe(true);
+ });
+
+ it('should return false when layout is a grid layout', () => {
+ const layout = AutoGridLayoutManager.createEmpty();
+ expect(containsTabsLayout(layout)).toBe(false);
+ });
+
+ it('should return false when layout is RowsLayoutManager with no tabs in rows', () => {
+ const layout = new RowsLayoutManager({
+ rows: [
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ ],
+ });
+ expect(containsTabsLayout(layout)).toBe(false);
+ });
+
+ it('should return true when RowsLayoutManager contains a row with tabs layout', () => {
+ const layout = new RowsLayoutManager({
+ rows: [
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ new RowItem({
+ layout: new TabsLayoutManager({
+ tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
+ }),
+ }),
+ ],
+ });
+ expect(containsTabsLayout(layout)).toBe(true);
+ });
+
+ it('should return true when any row contains tabs layout', () => {
+ const layout = new RowsLayoutManager({
+ rows: [
+ new RowItem({
+ layout: new TabsLayoutManager({
+ tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
+ }),
+ }),
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
+ ],
+ });
+ expect(containsTabsLayout(layout)).toBe(true);
+ });
+});
diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.ts b/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.ts
index 03dec1482fb..6050e25a94f 100644
--- a/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.ts
+++ b/public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.ts
@@ -15,3 +15,15 @@ export function findAllGridTypes(layout: DashboardLayoutManager): string[] {
return [];
}
+
+export function containsTabsLayout(layout: DashboardLayoutManager): boolean {
+ if (layout instanceof TabsLayoutManager) {
+ return true;
+ }
+
+ if (layout instanceof RowsLayoutManager) {
+ return layout.state.rows.some((row) => containsTabsLayout(row.getLayout()));
+ }
+
+ return false;
+}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 0ee16ef4483..45427183866 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -4614,6 +4614,7 @@
},
"canvas-actions": {
"add-panel": "Add panel",
+ "disabled-child-contains-tabs": "Cannot change to tabs because a row already contains tabs",
"disabled-nested-grouping": "Grouping is limited to 2 levels",
"disabled-nested-tabs": "Tabs cannot be nested inside other tabs",
"group-into-row": "Group into row",