You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ms...@apache.org on 2026/06/09 09:02:06 UTC

(superset) branch master updated: fix(theme): SDK theme config overrides dashboard-level theme in embedded mode (#40763)

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

msyavuz 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 d7ddf2023da fix(theme): SDK theme config overrides dashboard-level theme in embedded mode (#40763)
d7ddf2023da is described below

commit d7ddf2023da80ded52b881ef045089178645852f
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Tue Jun 9 12:01:57 2026 +0300

    fix(theme): SDK theme config overrides dashboard-level theme in embedded mode (#40763)
---
 .../packages/superset-core/src/theme/types.ts      |  6 +++
 .../src/components/CrudThemeProvider.test.tsx      | 55 ++++++++++++++++++++++
 .../src/components/CrudThemeProvider.tsx           | 19 ++++++--
 superset-frontend/src/theme/ThemeController.ts     | 15 ++++++
 superset-frontend/src/theme/ThemeProvider.tsx      |  9 +++-
 .../src/theme/tests/ThemeController.test.ts        | 18 +++++++
 .../src/theme/tests/ThemeProvider.test.tsx         |  1 +
 7 files changed, 118 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/packages/superset-core/src/theme/types.ts b/superset-frontend/packages/superset-core/src/theme/types.ts
index fccbfd41d36..e0e782d6497 100644
--- a/superset-frontend/packages/superset-core/src/theme/types.ts
+++ b/superset-frontend/packages/superset-core/src/theme/types.ts
@@ -508,6 +508,12 @@ export interface ThemeContextType {
   clearLocalOverrides: () => void;
   getCurrentCrudThemeId: () => string | null;
   hasDevOverride: () => boolean;
+  /**
+   * True when an explicit theme config override is active (e.g. supplied via
+   * the Embedded SDK). Such an override takes precedence over a
+   * dashboard-level theme.
+   */
+  hasThemeConfigOverride: boolean;
   canSetMode: () => boolean;
   canSetTheme: () => boolean;
   canDetectOSPreference: () => boolean;
diff --git a/superset-frontend/src/components/CrudThemeProvider.test.tsx b/superset-frontend/src/components/CrudThemeProvider.test.tsx
index cd61fbb2999..382c978fd9e 100644
--- a/superset-frontend/src/components/CrudThemeProvider.test.tsx
+++ b/superset-frontend/src/components/CrudThemeProvider.test.tsx
@@ -25,6 +25,8 @@ import {
   isThemeConfigDark,
 } from '@apache-superset/core/theme';
 import getBootstrapData from 'src/utils/getBootstrapData';
+import { ThemeContext } from 'src/theme/ThemeProvider';
+import type { ThemeContextType } from '@apache-superset/core/theme';
 import CrudThemeProvider from './CrudThemeProvider';
 
 jest.mock('@apache-superset/core/theme', () => ({
@@ -307,6 +309,59 @@ test('ignores non-array fontUrls in theme config without throwing', () => {
   expect(fontStyle).toBeNull();
 });
 
+test('skips the dashboard theme when an SDK theme config override is active', () => {
+  const themeConfig = {
+    token: {
+      colorPrimary: '#ff0000',
+      fontUrls: ['https://fonts.example.com/dashboard.css'],
+    },
+  };
+  render(
+    <ThemeContext.Provider
+      value={{ hasThemeConfigOverride: true } as unknown as ThemeContextType}
+    >
+      <CrudThemeProvider
+        theme={{
+          id: 1,
+          theme_name: 'Custom Theme',
+          json_data: JSON.stringify(themeConfig),
+        }}
+      >
+        <div>Dashboard Content</div>
+      </CrudThemeProvider>
+    </ThemeContext.Provider>,
+  );
+
+  // The SDK override wins: the dashboard theme provider must not wrap children.
+  expect(screen.getByText('Dashboard Content')).toBeInTheDocument();
+  expect(
+    screen.queryByTestId('dashboard-theme-provider'),
+  ).not.toBeInTheDocument();
+  // The override fully owns theming, so dashboard fonts must not be injected.
+  expect(document.querySelector('style[data-superset-fonts]')).toBeNull();
+});
+
+test('applies the dashboard theme when no SDK theme config override is active', () => {
+  const themeConfig = { token: { colorPrimary: '#ff0000' } };
+  render(
+    <ThemeContext.Provider
+      value={{ hasThemeConfigOverride: false } as unknown as ThemeContextType}
+    >
+      <CrudThemeProvider
+        theme={{
+          id: 1,
+          theme_name: 'Custom Theme',
+          json_data: JSON.stringify(themeConfig),
+        }}
+      >
+        <div>Dashboard Content</div>
+      </CrudThemeProvider>
+    </ThemeContext.Provider>,
+  );
+
+  expect(screen.getByTestId('dashboard-theme-provider')).toBeInTheDocument();
+});
+
 test('does not inject font style element when no fontUrls in config', () => {
   const themeConfig = { token: { colorPrimary: '#ff0000' } };
   render(
diff --git a/superset-frontend/src/components/CrudThemeProvider.tsx b/superset-frontend/src/components/CrudThemeProvider.tsx
index d6ed28d0e89..c89552147c4 100644
--- a/superset-frontend/src/components/CrudThemeProvider.tsx
+++ b/superset-frontend/src/components/CrudThemeProvider.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode, useEffect, useMemo } from 'react';
+import { ReactNode, useContext, useEffect, useMemo } from 'react';
 import { logging } from '@apache-superset/core/utils';
 import {
   Theme,
@@ -24,6 +24,7 @@ import {
   isThemeConfigDark,
 } from '@apache-superset/core/theme';
 import getBootstrapData from 'src/utils/getBootstrapData';
+import { ThemeContext } from 'src/theme/ThemeProvider';
 import type { Dashboard } from 'src/types/Dashboard';
 
 interface CrudThemeProviderProps {
@@ -41,8 +42,18 @@ export default function CrudThemeProvider({
   children,
   theme,
 }: CrudThemeProviderProps) {
+  // An explicit theme config override (e.g. supplied via the Embedded SDK)
+  // applies on the global theme controller and must win over the
+  // dashboard-level theme. When such an override is active, skip the
+  // dashboard theme so the override is not shadowed by this nested provider.
+  const themeContext = useContext(ThemeContext);
+  const hasThemeConfigOverride = themeContext?.hasThemeConfigOverride ?? false;
+
   const { dashboardTheme, fontUrls } = useMemo(() => {
-    if (!theme?.json_data) {
+    // When an SDK override is active it fully owns theming, so skip parsing the
+    // dashboard theme entirely. This also prevents the font-injection effect
+    // below from loading dashboard fonts the override does not use.
+    if (hasThemeConfigOverride || !theme?.json_data) {
       return { dashboardTheme: null, fontUrls: undefined };
     }
     try {
@@ -64,7 +75,7 @@ export default function CrudThemeProvider({
       logging.warn('Failed to load dashboard theme:', error);
       return { dashboardTheme: null, fontUrls: undefined };
     }
-  }, [theme?.json_data]);
+  }, [theme?.json_data, hasThemeConfigOverride]);
 
   useEffect(() => {
     if (!dashboardTheme || !fontUrls?.length) return undefined;
@@ -83,7 +94,7 @@ export default function CrudThemeProvider({
     };
   }, [dashboardTheme, fontUrls]);
 
-  if (!dashboardTheme) {
+  if (!dashboardTheme || hasThemeConfigOverride) {
     return <>{children}</>;
   }
 
diff --git a/superset-frontend/src/theme/ThemeController.ts b/superset-frontend/src/theme/ThemeController.ts
index 28d9939b5be..08d613fb3fe 100644
--- a/superset-frontend/src/theme/ThemeController.ts
+++ b/superset-frontend/src/theme/ThemeController.ts
@@ -99,6 +99,11 @@ export class ThemeController {
 
   private dashboardCrudTheme: AnyThemeConfig | null = null;
 
+  // Tracks whether an explicit theme config override has been applied via
+  // setThemeConfig (e.g. from the Embedded SDK). When set, it must take
+  // precedence over a dashboard-level theme.
+  private themeConfigOverride = false;
+
   // Track loaded font URLs to avoid duplicate injections
   private loadedFontUrls: Set<string> = new Set();
 
@@ -467,6 +472,15 @@ export class ThemeController {
     return this.devThemeOverride !== null;
   }
 
+  /**
+   * Checks if an explicit theme config override has been applied via
+   * setThemeConfig (e.g. from the Embedded SDK). When true, this override
+   * takes precedence over any dashboard-level theme.
+   */
+  public hasThemeConfigOverride(): boolean {
+    return this.themeConfigOverride;
+  }
+
   /**
    * Gets the applied theme ID (for UI display purposes).
    */
@@ -512,6 +526,7 @@ export class ThemeController {
   public setThemeConfig(config: SupersetThemeConfig): void {
     this.defaultTheme = config.theme_default;
     this.darkTheme = config.theme_dark || null;
+    this.themeConfigOverride = true;
 
     let newMode: ThemeMode;
     try {
diff --git a/superset-frontend/src/theme/ThemeProvider.tsx b/superset-frontend/src/theme/ThemeProvider.tsx
index ef979ff39bb..707729c1d6b 100644
--- a/superset-frontend/src/theme/ThemeProvider.tsx
+++ b/superset-frontend/src/theme/ThemeProvider.tsx
@@ -33,7 +33,7 @@ import {
 } from '@apache-superset/core/theme';
 import { ThemeController } from './ThemeController';
 
-const ThemeContext = createContext<ThemeContextType | null>(null);
+export const ThemeContext = createContext<ThemeContextType | null>(null);
 
 interface ThemeProviderProps {
   children: React.ReactNode;
@@ -52,6 +52,10 @@ export function SupersetThemeProvider({
     themeController.getCurrentMode(),
   );
 
+  const [hasThemeConfigOverride, setHasThemeConfigOverride] = useState<boolean>(
+    themeController.hasThemeConfigOverride(),
+  );
+
   useEffect(() => {
     // TODO: Once we migrate to react>=18 is should be possible
     // to replace the useState and useEffect with a singular
@@ -59,6 +63,7 @@ export function SupersetThemeProvider({
     const updateState = (theme: Theme) => {
       setCurrentTheme(theme);
       setCurrentThemeMode(themeController.getCurrentMode());
+      setHasThemeConfigOverride(themeController.hasThemeConfigOverride());
       document.documentElement.setAttribute(
         'data-theme-mode',
         themeController.getCurrentModeResolved(),
@@ -143,6 +148,7 @@ export function SupersetThemeProvider({
       clearLocalOverrides,
       getCurrentCrudThemeId,
       hasDevOverride,
+      hasThemeConfigOverride,
       canSetMode,
       canSetTheme,
       canDetectOSPreference,
@@ -159,6 +165,7 @@ export function SupersetThemeProvider({
       clearLocalOverrides,
       getCurrentCrudThemeId,
       hasDevOverride,
+      hasThemeConfigOverride,
       canSetMode,
       canSetTheme,
       canDetectOSPreference,
diff --git a/superset-frontend/src/theme/tests/ThemeController.test.ts b/superset-frontend/src/theme/tests/ThemeController.test.ts
index b427107772d..659c68a9f48 100644
--- a/superset-frontend/src/theme/tests/ThemeController.test.ts
+++ b/superset-frontend/src/theme/tests/ThemeController.test.ts
@@ -1082,6 +1082,24 @@ test('setThemeConfig sets complete theme configuration', () => {
   expect(controller.canSetMode()).toBe(true);
 });
 
+test('setThemeConfig flags an active theme config override', () => {
+  mockGetBootstrapData.mockReturnValue(
+    createMockBootstrapData({ default: {}, dark: {} }),
+  );
+
+  const controller = createController({ defaultTheme: { token: {} } });
+
+  // No override until setThemeConfig is called (e.g. from the Embedded SDK).
+  expect(controller.hasThemeConfigOverride()).toBe(false);
+
+  controller.setThemeConfig({
+    theme_default: DEFAULT_THEME,
+    theme_dark: DARK_THEME,
+  });
+
+  expect(controller.hasThemeConfigOverride()).toBe(true);
+});
+
 test('setThemeConfig handles theme_default only', () => {
   mockGetBootstrapData.mockReturnValue(
     createMockBootstrapData({
diff --git a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
index a15cd5289f9..07b3e84fcda 100644
--- a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
+++ b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
@@ -86,6 +86,7 @@ describe('SupersetThemeProvider', () => {
       clearLocalOverrides: jest.fn(),
       getCurrentCrudThemeId: jest.fn().mockReturnValue(null),
       hasDevOverride: jest.fn().mockReturnValue(false),
+      hasThemeConfigOverride: jest.fn().mockReturnValue(false),
       canSetMode: jest.fn().mockReturnValue(true),
       canSetTheme: jest.fn().mockReturnValue(true),
       canDetectOSPreference: jest.fn().mockReturnValue(true),