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/04 10:57:30 UTC

(superset) branch msyavuz/fix/theme-embedded-override created (now aaf69673c71)

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

msyavuz pushed a change to branch msyavuz/fix/theme-embedded-override
in repository https://gitbox.apache.org/repos/asf/superset.git


      at aaf69673c71 fix(theme): embedded method overrides dashboard level config

This branch includes the following new commits:

     new aaf69673c71 fix(theme): embedded method overrides dashboard level config

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fix(theme): embedded method overrides dashboard level config

Posted by ms...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch msyavuz/fix/theme-embedded-override
in repository https://gitbox.apache.org/repos/asf/superset.git

commit aaf69673c719291f89aa669eca104435457945e3
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Thu Jun 4 13:55:41 2026 +0300

    fix(theme): embedded method overrides dashboard level config
---
 .../packages/superset-core/src/theme/types.ts      |  6 +++
 .../src/components/CrudThemeProvider.test.tsx      | 48 ++++++++++++++++++++++
 .../src/components/CrudThemeProvider.tsx           | 12 +++++-
 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, 106 insertions(+), 3 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..76ece20cadc 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,52 @@ 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' } };
+  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();
+});
+
+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..3fba1e6237f 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,6 +42,13 @@ 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) {
       return { dashboardTheme: null, fontUrls: undefined };
@@ -83,7 +91,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),