This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch msyavuz/feat/canvas
in repository https://gitbox.apache.org/repos/asf/superset.git

commit bf7d0fe3f608e2d3ca78713b49442fffcfda6f3d
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Thu Oct 1 15:42:17 2026 +0300

    feat(canvas): drop the arbitrary definition limits
    
    The caps on node count, nesting depth, operations per request, widget
    id length and the upper bounds on grid and settings values were never
    calibrated against real dashboards, so they're removed. Only structural
    minimums remain. Reading order is computed iteratively so deep trees
    can't exhaust the recursion limit.
---
 superset/canvas/definition/schemas.py      | 20 +++++++++-----------
 superset/canvas/definition/scopes.py       | 13 +++++--------
 superset/canvas/definition/validation.py   | 17 -----------------
 tests/unit_tests/canvas/validation_test.py | 13 +++++++++++++
 4 files changed, 27 insertions(+), 36 deletions(-)

diff --git a/superset/canvas/definition/schemas.py 
b/superset/canvas/definition/schemas.py
index 1bc55ba31ba..0522f2a865d 100644
--- a/superset/canvas/definition/schemas.py
+++ b/superset/canvas/definition/schemas.py
@@ -46,9 +46,9 @@ class _Model(BaseModel):
 
 
 class RootLayout(_Model):
-    columns: int = Field(default=24, ge=1, le=48)
-    gap: int = Field(default=16, ge=0, le=64)
-    row_unit: int = Field(default=40, ge=8, le=400)
+    columns: int = Field(default=24, ge=1)
+    gap: int = Field(default=16, ge=0)
+    row_unit: int = Field(default=40, ge=1)
 
 
 class Root(_Model):
@@ -58,7 +58,7 @@ class Root(_Model):
 
 class Node(_Model):
     # Id of the placed widget.
-    widget: str = Field(min_length=1, max_length=64)
+    widget: str = Field(min_length=1)
     # Placement within the parent, validated against the parent's rules.
     layout: dict[str, Any] = Field(default_factory=dict)
     # Present only when the widget is a container.
@@ -112,10 +112,10 @@ class Interactions(_Model):
 
 class RefreshSettings(_Model):
     # Seconds between automatic refreshes of every widget's data; 0 is off.
-    interval: int = Field(default=0, ge=0, le=7 * 24 * 60 * 60)
+    interval: int = Field(default=0, ge=0)
     # Milliseconds a refresh is spread over across widgets; 0 refreshes all at
     # once.
-    stagger: int = Field(default=0, ge=0, le=10 * 60 * 1000)
+    stagger: int = Field(default=0, ge=0)
     # Nodes left out of automatic refresh.
     exempt: list[str] = Field(default_factory=list)
 
@@ -124,9 +124,7 @@ class ColorSettings(_Model):
     # Categorical color scheme shared by every widget; the default when unset.
     scheme: str | None = Field(default=None, min_length=1, max_length=255)
     # Fixed colors for series labels across widgets, e.g. {"France": 
"#1f77b4"}.
-    label_colors: dict[str, Annotated[str, Field(max_length=64)]] = Field(
-        default_factory=dict, max_length=1000
-    )
+    label_colors: dict[str, str] = Field(default_factory=dict)
 
 
 class DisplaySettings(_Model):
@@ -171,7 +169,7 @@ class AddOp(_Model):
 
     op: Literal["add"]
     id: str | None = Field(default=None, pattern=NODE_ID_PATTERN)
-    widget: str = Field(min_length=1, max_length=64)
+    widget: str = Field(min_length=1)
     layout: dict[str, Any] = Field(default_factory=dict)
     parent: str = ROOT_ID
     # Position in the parent's children (reading order); appended when omitted.
@@ -235,4 +233,4 @@ class ApplyOperationsRequest(BaseModel):
 
     # The revision the caller's view of the canvas is based on.
     base_revision: int = Field(ge=0)
-    ops: list[Operation] = Field(min_length=1, max_length=200)
+    ops: list[Operation] = Field(min_length=1)
diff --git a/superset/canvas/definition/scopes.py 
b/superset/canvas/definition/scopes.py
index 2fe94c996de..1ad677451f4 100644
--- a/superset/canvas/definition/scopes.py
+++ b/superset/canvas/definition/scopes.py
@@ -62,15 +62,12 @@ SCOPE_RESULT_KEYS = {
 
 def _reading_order(canvas: dict[str, Any], start: str) -> list[str]:
     """Node ids under ``start`` (exclusive), depth first in children order."""
-    children = (
-        canvas["root"]["children"]
-        if start == ROOT_ID
-        else canvas["nodes"][start].get("children") or []
-    )
     ordered: list[str] = []
-    for child in children:
-        ordered.append(child)
-        ordered.extend(_reading_order(canvas, child))
+    stack = list(reversed(_children(canvas, start)))
+    while stack:
+        node_id = stack.pop()
+        ordered.append(node_id)
+        stack.extend(reversed(_children(canvas, node_id)))
     return ordered
 
 
diff --git a/superset/canvas/definition/validation.py 
b/superset/canvas/definition/validation.py
index 03ec67dda41..d04a5440e71 100644
--- a/superset/canvas/definition/validation.py
+++ b/superset/canvas/definition/validation.py
@@ -53,9 +53,6 @@ from superset.canvas.definition.schemas import (
     SCOPE_FIELDS,
 )
 
-MAX_NODES = 500
-MAX_DEPTH = 8
-
 # The layout rule a node needs to own each kind of scope override.
 SCOPE_ROLES = {
     "filter": ("is_filter", "a filter"),
@@ -123,12 +120,9 @@ class _Validator:
             raise DefinitionValidationError(_pydantic_issues((), ex)) from ex
         doc = canvas.model_dump(mode="json", by_alias=True, exclude_none=True)
 
-        if len(doc["nodes"]) > MAX_NODES:
-            self.fail("/nodes", f"more than {MAX_NODES} nodes")
         self._check_tree(doc)
         self._check_widgets(doc)
         self._check_nesting()
-        self._check_depth(doc)
         self._check_interactions(doc)
         self._check_settings(doc)
         if not self.issues:
@@ -262,17 +256,6 @@ class _Validator:
                     f"unknown node {node_id!r}",
                 )
 
-    def _check_depth(self, doc: dict[str, Any]) -> None:
-        for node_id in doc["nodes"]:
-            depth, current = 1, node_id
-            while self.parents.get(current, ROOT_ID) != ROOT_ID and depth <= 
MAX_DEPTH:
-                current = self.parents[current]
-                depth += 1
-            if depth > MAX_DEPTH:
-                self.fail(
-                    pointer("nodes", node_id), f"nested deeper than 
{MAX_DEPTH} levels"
-                )
-
     def _resolve_layouts(self, doc: dict[str, Any]) -> None:
         self._resolve_children(doc, ROOT_ID, doc["root"]["layout"]["columns"], 
None)
         for node_id, rules in self.node_rules.items():
diff --git a/tests/unit_tests/canvas/validation_test.py 
b/tests/unit_tests/canvas/validation_test.py
index a9ec1e1035f..681b0e5ef60 100644
--- a/tests/unit_tests/canvas/validation_test.py
+++ b/tests/unit_tests/canvas/validation_test.py
@@ -244,3 +244,16 @@ def test_settings_are_validated() -> None:
         "/settings/refresh/interval",
         "/settings/colors/theme",
     }
+
+
+def test_nesting_depth_is_not_capped() -> None:
+    nodes = {
+        f"g{level}": node(
+            f"group-{level}", children=[f"g{level + 1}"] if level < 29 else []
+        )
+        for level in range(30)
+    }
+
+    result = normalize(canvas(nodes, children=["g0"]))
+
+    assert len(result["nodes"]) == 30

Reply via email to