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"""