{"record":{"id":"6eab8a599245c74d","repo":"grafana/grafana","slug":"layout-nesting-too-deep-max-10-levels","errorCode":null,"errorMessage":"Layout nesting too deep (max 10 levels)","messagePattern":"Layout nesting too deep \\(max 10 levels\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"public/app/features/dashboard-scene/mutation-api/commands/movePanelsHelper.ts","lineNumber":72,"sourceCode":"        throw new Error('Cannot move panels to an empty RowsLayout (no rows to receive panels)');\n      }\n      current = firstRow.state.layout;\n      continue;\n    }\n\n    if (current instanceof TabsLayoutManager) {\n      const firstTab = current.state.tabs[0];\n      if (!firstTab) {\n        throw new Error('Cannot move panels to an empty TabsLayout (no tabs to receive panels)');\n      }\n      current = firstTab.state.layout;\n      continue;\n    }\n\n    throw new Error(`Cannot move panels to layout type: ${current.constructor.name}`);\n  }\n\n  throw new Error('Layout nesting too deep (max 10 levels)');\n}\n\n/**\n * Move panels from one layout to a target layout without changing panel IDs.\n *\n * Panels are cloned (so they can be detached from their current parent) and\n * added directly to the target grid's children, preserving their existing\n * keys and grid positions.\n *\n * @param panels - VizPanels to relocate\n * @param targetLayout - The layout manager at the moveContentTo path\n */\nexport function movePanelsToLayout(panels: VizPanel[], targetLayout: DashboardLayoutManager): void {\n  if (panels.length === 0) {\n    return;\n  }\n\n  const leaf = resolveLeafLayout(targetLayout);","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/grafana/grafana/blob/ae3104e3690e28e72c8f0c97efc12647a70f75c3/public/app/features/dashboard-scene/mutation-api/commands/movePanelsHelper.ts#L54-L90","documentation":"Thrown by resolveLeafLayout() in movePanelsHelper.ts while descending a layout tree to find a leaf grid (DefaultGridLayoutManager or AutoGridLayoutManager) that can directly hold panels. The for-loop runs at most 10 iterations; if it never reaches a leaf grid after descending through RowsLayoutManager/TabsLayoutManager containers 10 times, it throws. This is a defensive depth/cycle guard used by movePanelsToLayout, which is invoked from REMOVE_ROW and REMOVE_TAB when moveContentTo is set. Note: normal dashboards cap group nesting at MAX_NESTING_DEPTH=4, so hitting 10 implies corrupted, hand-edited, or legacy-migrated data.","triggerScenarios":"Calling REMOVE_ROW or REMOVE_TAB with a moveContentTo path whose resolved layout manager sits atop more than 10 nested RowsLayoutManager/TabsLayoutManager layers without any DefaultGridLayoutManager/AutoGridLayoutManager in between. Also reachable via MOVE_PANEL through the same helper on a deeply malformed target.","commonSituations":"A dashboard JSON hand-edited or produced by an older migration path that bypassed the MAX_NESTING_DEPTH=4 nesting validation; a corrupted scene graph where a cycle of rows/tabs references each other; imported dashboards from external tools that emit pathological nesting.","solutions":["Call GET_LAYOUT first to inspect the actual structure at the moveContentTo path and confirm the depth is sane; if it exceeds 4 group layers the dashboard is already invalid and needs repair.","Point moveContentTo at a shallower, valid group (e.g. the root \"/\") that resolves to a grid layout instead of a deeply nested container.","Repair the corrupted layout tree before retrying: flatten the excess nesting through UPDATE_LAYOUT conversions or rebuild the dashboard from a known-good save model.","Omit moveContentTo so the contained panels are deleted with the row/tab rather than relocated through the broken tree."],"exampleFix":"// before: moveContentTo points into a corrupt deep structure\nmutationApi.execute('REMOVE_ROW', { path: '/rows/0', moveContentTo: '/tabs/0/rows/0/tabs/0/rows/0/tabs/0/rows/0' });\n\n// after: relocate to the root grid, which is a valid leaf\nmutationApi.execute('REMOVE_ROW', { path: '/rows/0', moveContentTo: '/' });","handlingStrategy":"validation","validationCode":"// Before REMOVE_ROW/REMOVE_TAB with moveContentTo, fetch layout and sanity-check depth\nconst layoutRes = await mutationApi.execute('GET_LAYOUT', {});\nfunction maxDepth(node, d = 0) {\n  if (!node.children) return d;\n  return Math.max(...node.children.map((c) => maxDepth(c, d + 1)), d);\n}\nconst target = findNodeByPath(layoutRes.data.layout, moveContentTo);\nif (maxDepth(target) > 4) {\n  throw new Error('Target layout is too deeply nested; choose a shallower path.');\n}","typeGuard":"null","tryCatchPattern":"const res = await mutationApi.execute('REMOVE_ROW', { path, moveContentTo });\nif (!res.success && res.error.includes('nesting too deep')) {\n  // fall back to omitting moveContentTo (delete content) or use root\n  await mutationApi.execute('REMOVE_ROW', { path });\n}","preventionTips":["Always call GET_LAYOUT before operations that use moveContentTo to verify the target structure.","Prefer shallow moveContentTo paths (root or top-level groups) to avoid deep traversal.","Repair dashboards whose group nesting exceeds MAX_NESTING_DEPTH=4 before attempting mutations."],"tags":["dashboard-scene","mutation-api","layout","nesting","validation"],"backgroundTag":null,"analyzedSha":"ae3104e3690e28e72c8f0c97efc12647a70f75c3","analyzedAt":"2026-08-12T12:48:38.752Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}