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

sadpandajoe pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d4a31d1d78a fix: add subdirectory deployment support for 
brandSpinnerUrl (#37523)
d4a31d1d78a is described below

commit d4a31d1d78a2f05a4e1c384c8f56c6dbdb65170c
Author: innovark <[email protected]>
AuthorDate: Tue Jul 14 00:01:14 2026 +0300

    fix: add subdirectory deployment support for brandSpinnerUrl (#37523)
    
    Co-authored-by: Evan Rusackas <[email protected]>
---
 superset/views/base.py                            |  27 +++++
 tests/unit_tests/views/test_base_theme_helpers.py | 132 ++++++++++++++++++++++
 2 files changed, 159 insertions(+)

diff --git a/superset/views/base.py b/superset/views/base.py
index c8106a9e6ba..4100976df13 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -649,6 +649,27 @@ def get_spa_payload(extra_data: dict[str, Any] | None = 
None) -> dict[str, Any]:
     return payload
 
 
+def _ensure_static_assets_prefix(url_or_path: str) -> str:
+    """Add the configured static asset prefix to root-relative asset paths."""
+    static_assets_prefix = app.config.get("STATIC_ASSETS_PREFIX", "")
+
+    if (
+        url_or_path.startswith("//")
+        or not url_or_path.startswith("/")
+        or not static_assets_prefix
+    ):
+        return url_or_path
+
+    normalized_prefix = static_assets_prefix.rstrip("/")
+    if normalized_prefix and (
+        url_or_path == normalized_prefix
+        or url_or_path.startswith(f"{normalized_prefix}/")
+    ):
+        return url_or_path
+
+    return f"{normalized_prefix}{url_or_path}"
+
+
 def get_spa_template_context(
     entry: str | None = "spa",
     extra_bootstrap_data: dict[str, Any] | None = None,
@@ -694,6 +715,12 @@ def get_spa_template_context(
                 # User has customized APP_NAME, use it as brandAppName
                 theme_tokens["brandAppName"] = app_name_from_config
 
+        brand_spinner_url = theme_tokens.get("brandSpinnerUrl")
+        if isinstance(brand_spinner_url, str) and brand_spinner_url:
+            theme_tokens["brandSpinnerUrl"] = _ensure_static_assets_prefix(
+                brand_spinner_url
+            )
+
     # Write the modified theme data back to payload
     if "common" not in payload:
         payload["common"] = {}
diff --git a/tests/unit_tests/views/test_base_theme_helpers.py 
b/tests/unit_tests/views/test_base_theme_helpers.py
index fb968d5d1e4..08b714ae2ec 100644
--- a/tests/unit_tests/views/test_base_theme_helpers.py
+++ b/tests/unit_tests/views/test_base_theme_helpers.py
@@ -18,6 +18,7 @@
 from unittest.mock import MagicMock, mock_open, patch
 
 from superset.themes.types import ThemeMode
+from superset.utils import json
 from superset.views.base import (
     _load_theme_from_model,
     _merge_theme_dicts,
@@ -892,6 +893,137 @@ class TestBrandAppNameFallback:
         assert result["default_title"] == "Superset"
 
 
+class TestBrandSpinnerUrlPrefix:
+    """Test brandSpinnerUrl static asset prefix handling."""
+
+    @patch("superset.views.base.get_spa_payload")
+    @patch("superset.views.base.app")
+    def test_brandspinnerurl_adds_static_assets_prefix(self, mock_app, 
mock_payload):
+        """Test that root-relative spinner URLs include the static assets 
prefix."""
+        from superset.views.base import get_spa_template_context
+
+        mock_app.config = {
+            "STATIC_ASSETS_PREFIX": "/analytics",
+        }
+        mock_payload.return_value = {
+            "common": {
+                "theme": {
+                    "default": {
+                        "token": {
+                            "brandSpinnerUrl": "/static/assets/spinner.gif",
+                        }
+                    },
+                    "dark": {
+                        "token": {
+                            "brandSpinnerUrl": 
"/static/assets/dark-spinner.gif",
+                        }
+                    },
+                }
+            }
+        }
+
+        result = get_spa_template_context("app")
+
+        assert (
+            result["theme_tokens"]["brandSpinnerUrl"]
+            == "/analytics/static/assets/spinner.gif"
+        )
+        bootstrap_data = json.loads(result["bootstrap_data"])
+        assert (
+            
bootstrap_data["common"]["theme"]["default"]["token"]["brandSpinnerUrl"]
+            == "/analytics/static/assets/spinner.gif"
+        )
+        assert (
+            
bootstrap_data["common"]["theme"]["dark"]["token"]["brandSpinnerUrl"]
+            == "/analytics/static/assets/dark-spinner.gif"
+        )
+
+    @patch("superset.views.base.get_spa_payload")
+    @patch("superset.views.base.app")
+    def test_brandspinnerurl_keeps_absolute_url(self, mock_app, mock_payload):
+        """Test that absolute spinner URLs are not prefixed."""
+        from superset.views.base import get_spa_template_context
+
+        mock_app.config = {
+            "STATIC_ASSETS_PREFIX": "/analytics",
+        }
+        mock_payload.return_value = {
+            "common": {
+                "theme": {
+                    "default": {
+                        "token": {
+                            "brandSpinnerUrl": 
"https://cdn.example.com/spinner.gif";,
+                        }
+                    }
+                }
+            }
+        }
+
+        result = get_spa_template_context("app")
+
+        assert (
+            result["theme_tokens"]["brandSpinnerUrl"]
+            == "https://cdn.example.com/spinner.gif";
+        )
+
+    @patch("superset.views.base.get_spa_payload")
+    @patch("superset.views.base.app")
+    def test_brandspinnerurl_keeps_protocol_relative_url(self, mock_app, 
mock_payload):
+        """Test that protocol-relative spinner URLs are not prefixed."""
+        from superset.views.base import get_spa_template_context
+
+        mock_app.config = {
+            "STATIC_ASSETS_PREFIX": "/analytics",
+        }
+        mock_payload.return_value = {
+            "common": {
+                "theme": {
+                    "default": {
+                        "token": {
+                            "brandSpinnerUrl": "//cdn.example.com/spinner.gif",
+                        }
+                    }
+                }
+            }
+        }
+
+        result = get_spa_template_context("app")
+
+        assert (
+            result["theme_tokens"]["brandSpinnerUrl"] == 
"//cdn.example.com/spinner.gif"
+        )
+
+    @patch("superset.views.base.get_spa_payload")
+    @patch("superset.views.base.app")
+    def test_brandspinnerurl_does_not_duplicate_static_assets_prefix(
+        self, mock_app, mock_payload
+    ):
+        """Test that already-prefixed spinner URLs are not prefixed again."""
+        from superset.views.base import get_spa_template_context
+
+        mock_app.config = {
+            "STATIC_ASSETS_PREFIX": "/analytics",
+        }
+        mock_payload.return_value = {
+            "common": {
+                "theme": {
+                    "default": {
+                        "token": {
+                            "brandSpinnerUrl": 
"/analytics/static/assets/spinner.gif",
+                        }
+                    }
+                }
+            }
+        }
+
+        result = get_spa_template_context("app")
+
+        assert (
+            result["theme_tokens"]["brandSpinnerUrl"]
+            == "/analytics/static/assets/spinner.gif"
+        )
+
+
 class TestGetDefaultSpinnerSvg:
     """Test get_default_spinner_svg function"""
 

Reply via email to