diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 79257c3d6da..6994d45fb40 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -396,6 +396,10 @@ export interface FeatureToggles { */ dashboardUndoRedo?: boolean; /** + * Enables unlimited dashboard panel grouping + */ + unlimitedLayoutsNesting?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 567c6d566e7..01b37ad9386 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -657,6 +657,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "unlimitedLayoutsNesting", + Description: "Enables unlimited dashboard panel grouping", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "panelFilterVariable", Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0118448325c..88af45a4d15 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -87,6 +87,7 @@ dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true +unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index fbc93fc3fc9..f26a754d574 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -359,6 +359,10 @@ const ( // Enables undo/redo in dynamic dashboards FlagDashboardUndoRedo = "dashboardUndoRedo" + // FlagUnlimitedLayoutsNesting + // Enables unlimited dashboard panel grouping + FlagUnlimitedLayoutsNesting = "unlimitedLayoutsNesting" + // FlagPanelFilterVariable // Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard FlagPanelFilterVariable = "panelFilterVariable" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index d60e4938bf0..caf8239adce 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3897,6 +3897,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "unlimitedLayoutsNesting", + "resourceVersion": "1760013838902", + "creationTimestamp": "2025-10-09T12:43:58Z" + }, + "spec": { + "description": "Enables unlimited dashboard panel grouping", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "useKubernetesShortURLsAPI", 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 47c5adb2453..d5d43c90ca1 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx @@ -1,8 +1,10 @@ import { css, cx } from '@emotion/css'; +import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; @@ -11,7 +13,7 @@ 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 { DashboardLayoutManager, isDashboardLayoutManager } from '../types/DashboardLayoutManager'; import { addNewRowTo, addNewTabTo } from './addNew'; import { useClipboardState } from './useClipboardState'; @@ -25,6 +27,38 @@ export function CanvasGridAddActions({ layoutManager }: Props) { const styles = useStyles2(getStyles); const { hasCopiedPanel } = useClipboardState(); + const { disableGrouping, disableTabs } = useMemo(() => { + if (config.featureToggles.unlimitedLayoutsNesting) { + return { disableGrouping: false, disableTabs: false }; + } + + let parent = layoutManager.parent; + const layouts = []; + + while (parent) { + if (isDashboardLayoutManager(parent)) { + layouts.push(parent.descriptor.id); + } + + if (layouts.length === 2) { + parent = undefined; + break; + } + + parent = parent.parent; + } + + if (layouts.length === 2) { + return { disableGrouping: true, disableTabs: true }; + } + + if (layouts.length === 1 && layouts[0] === TabsLayoutManager.descriptor.id) { + return { disableGrouping: false, disableTabs: true }; + } + + return { disableGrouping: false, disableTabs: false }; + }, [layoutManager]); + return (
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 125bd380256..f4714e7d420 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -1,10 +1,12 @@ import { useCallback, useMemo } from 'react'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { RadioButtonGroup, Box } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -19,6 +21,21 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { const isGridLayout = layoutManager.descriptor.isGridLayout; const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout); + const disableTabs = useMemo(() => { + if (config.featureToggles.unlimitedLayoutsNesting) { + return false; + } + let parent = layoutManager.parent; + while (parent) { + if (parent instanceof TabsLayoutManager) { + return true; + } + parent = parent.parent; + } + + return false; + }, [layoutManager]); + const onChangeLayout = useCallback( (newLayout: LayoutRegistryItem) => { const layoutParent = layoutManager.parent; @@ -30,17 +47,33 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { [layoutManager] ); - const radioOptions = options.map((opt) => ({ - value: opt, - label: opt.name, - icon: opt.icon, - description: opt.description, - ariaLabel: `layout-selection-option-${opt.name}`, - })); + const disabledOptions: LayoutRegistryItem[] = []; + + 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'); + disabledOptions.push(opt); + } + + return { + value: opt, + label: opt.name, + icon: opt.icon, + description, + ariaLabel: `layout-selection-option-${opt.name}`, + }; + }); return ( - + ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 865e80a7934..189ec56399c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4496,6 +4496,8 @@ }, "canvas-actions": { "add-panel": "Add panel", + "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", "group-into-tab": "Group into tab", "group-panels": "Group panels",