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),