mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat(theming): add base theme config (#35220)
This commit is contained in:
committed by
GitHub
parent
ef78d2af06
commit
220480b627
@@ -18,18 +18,15 @@
|
||||
*/
|
||||
import {
|
||||
type AnyThemeConfig,
|
||||
type SupersetTheme,
|
||||
type SupersetThemeConfig,
|
||||
type ThemeControllerOptions,
|
||||
type ThemeStorage,
|
||||
isThemeConfigDark,
|
||||
Theme,
|
||||
ThemeMode,
|
||||
themeObject as supersetThemeObject,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getAntdConfig,
|
||||
normalizeThemeConfig,
|
||||
} from '@superset-ui/core/theme/utils';
|
||||
import { normalizeThemeConfig } from '@superset-ui/core/theme/utils';
|
||||
import type {
|
||||
BootstrapThemeData,
|
||||
BootstrapThemeDataConfig,
|
||||
@@ -79,7 +76,7 @@ export class ThemeController {
|
||||
|
||||
private modeStorageKey: string;
|
||||
|
||||
private defaultTheme: AnyThemeConfig;
|
||||
private defaultTheme: AnyThemeConfig | null;
|
||||
|
||||
private darkTheme: AnyThemeConfig | null;
|
||||
|
||||
@@ -87,8 +84,6 @@ export class ThemeController {
|
||||
|
||||
private currentMode: ThemeMode;
|
||||
|
||||
private hasCustomThemes: boolean;
|
||||
|
||||
private onChangeCallbacks: Set<(theme: Theme) => void> = new Set();
|
||||
|
||||
private mediaQuery: MediaQueryList;
|
||||
@@ -116,22 +111,13 @@ export class ThemeController {
|
||||
this.globalTheme = themeObject;
|
||||
|
||||
// Initialize bootstrap data and themes
|
||||
const {
|
||||
bootstrapDefaultTheme,
|
||||
bootstrapDarkTheme,
|
||||
hasCustomThemes,
|
||||
}: BootstrapThemeData = this.loadBootstrapData();
|
||||
const { bootstrapDefaultTheme, bootstrapDarkTheme }: BootstrapThemeData =
|
||||
this.loadBootstrapData();
|
||||
|
||||
this.hasCustomThemes = hasCustomThemes;
|
||||
|
||||
// Set themes based on bootstrap data availability
|
||||
if (this.hasCustomThemes) {
|
||||
this.darkTheme = bootstrapDarkTheme;
|
||||
this.defaultTheme = bootstrapDefaultTheme || defaultTheme;
|
||||
} else {
|
||||
this.darkTheme = null;
|
||||
this.defaultTheme = defaultTheme;
|
||||
}
|
||||
// Set themes from bootstrap data
|
||||
// These will be the THEME_DEFAULT and THEME_DARK from config
|
||||
this.defaultTheme = bootstrapDefaultTheme || defaultTheme || null;
|
||||
this.darkTheme = bootstrapDarkTheme;
|
||||
|
||||
// Initialize system theme detection
|
||||
this.systemMode = ThemeController.getSystemPreferredMode();
|
||||
@@ -147,7 +133,7 @@ export class ThemeController {
|
||||
// Initialize theme and mode
|
||||
this.currentMode = this.determineInitialMode();
|
||||
const initialTheme =
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme;
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme || {};
|
||||
|
||||
// Setup change callback
|
||||
if (onChange) this.onChangeCallbacks.add(onChange);
|
||||
@@ -197,6 +183,7 @@ export class ThemeController {
|
||||
|
||||
/**
|
||||
* Gets the theme configuration for a specific context (global vs dashboard).
|
||||
* Dashboard themes are always merged with base theme.
|
||||
* @param forDashboard - Whether to get the dashboard theme or global theme
|
||||
* @returns The theme configuration for the specified context
|
||||
*/
|
||||
@@ -205,7 +192,16 @@ export class ThemeController {
|
||||
): AnyThemeConfig | null {
|
||||
// For dashboard context, prioritize dashboard CRUD theme
|
||||
if (forDashboard && this.dashboardCrudTheme) {
|
||||
return this.dashboardCrudTheme;
|
||||
// Dashboard CRUD themes should be merged with base theme
|
||||
const normalizedTheme = this.normalizeTheme(this.dashboardCrudTheme);
|
||||
const isDarkMode = isThemeConfigDark(normalizedTheme);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
if (baseTheme) {
|
||||
const mergedTheme = Theme.fromConfig(normalizedTheme, baseTheme);
|
||||
return mergedTheme.toSerializedConfig();
|
||||
}
|
||||
return normalizedTheme;
|
||||
}
|
||||
|
||||
// For global context or when no dashboard theme, use mode-based theme
|
||||
@@ -241,7 +237,15 @@ export class ThemeController {
|
||||
// Controller creates and owns the dashboard theme
|
||||
const { Theme } = await import('@superset-ui/core');
|
||||
const normalizedConfig = this.normalizeTheme(themeConfig);
|
||||
const dashboardTheme = Theme.fromConfig(normalizedConfig);
|
||||
|
||||
// Determine if this is a dark theme and get appropriate base
|
||||
const isDarkMode = isThemeConfigDark(normalizedConfig);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
const dashboardTheme = Theme.fromConfig(
|
||||
normalizedConfig,
|
||||
baseTheme || undefined,
|
||||
);
|
||||
|
||||
// Cache the theme for reuse
|
||||
this.dashboardThemes.set(themeId, dashboardTheme);
|
||||
@@ -325,7 +329,7 @@ export class ThemeController {
|
||||
public resetTheme(): void {
|
||||
this.currentMode = ThemeMode.DEFAULT;
|
||||
const defaultTheme: AnyThemeConfig =
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme || {};
|
||||
|
||||
this.updateTheme(defaultTheme);
|
||||
}
|
||||
@@ -373,8 +377,8 @@ export class ThemeController {
|
||||
JSON.stringify(theme),
|
||||
);
|
||||
|
||||
const normalizedTheme = this.normalizeTheme(theme);
|
||||
this.updateTheme(normalizedTheme);
|
||||
const mergedTheme = this.getThemeForMode(this.currentMode);
|
||||
if (mergedTheme) this.updateTheme(mergedTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,10 +388,14 @@ export class ThemeController {
|
||||
public clearLocalOverrides(): void {
|
||||
this.devThemeOverride = null;
|
||||
this.crudThemeId = null;
|
||||
this.dashboardCrudTheme = null;
|
||||
|
||||
this.storage.removeItem(STORAGE_KEYS.DEV_THEME_OVERRIDE);
|
||||
this.storage.removeItem(STORAGE_KEYS.CRUD_THEME_ID);
|
||||
|
||||
// Clear dashboard themes cache
|
||||
this.dashboardThemes.clear();
|
||||
|
||||
this.resetTheme();
|
||||
}
|
||||
|
||||
@@ -407,7 +415,7 @@ export class ThemeController {
|
||||
|
||||
/**
|
||||
* Checks if OS preference detection is allowed.
|
||||
* Allowed when both themes are available
|
||||
* Allowed when dark theme is available (including base dark theme)
|
||||
*/
|
||||
public canDetectOSPreference(): boolean {
|
||||
return this.darkTheme !== null;
|
||||
@@ -422,7 +430,6 @@ export class ThemeController {
|
||||
public setThemeConfig(config: SupersetThemeConfig): void {
|
||||
this.defaultTheme = config.theme_default;
|
||||
this.darkTheme = config.theme_dark || null;
|
||||
this.hasCustomThemes = true;
|
||||
|
||||
let newMode: ThemeMode;
|
||||
try {
|
||||
@@ -478,13 +485,16 @@ export class ThemeController {
|
||||
private updateTheme(theme?: AnyThemeConfig): void {
|
||||
try {
|
||||
// If no config provided, use current mode to get theme
|
||||
const config: AnyThemeConfig =
|
||||
theme || this.getThemeForMode(this.currentMode) || this.defaultTheme;
|
||||
if (!theme) {
|
||||
// No theme provided, use the current mode's theme
|
||||
const modeTheme =
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme || {};
|
||||
this.applyTheme(modeTheme);
|
||||
} else {
|
||||
// Theme provided, apply it directly
|
||||
this.applyTheme(theme);
|
||||
}
|
||||
|
||||
// Normalize the theme
|
||||
const normalizedTheme = this.normalizeTheme(config);
|
||||
|
||||
this.applyTheme(normalizedTheme);
|
||||
this.persistMode();
|
||||
this.notifyListeners();
|
||||
} catch (error) {
|
||||
@@ -501,7 +511,7 @@ export class ThemeController {
|
||||
|
||||
// Get the default theme which will have the correct algorithm
|
||||
const defaultTheme: AnyThemeConfig =
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme || {};
|
||||
|
||||
this.applyTheme(defaultTheme);
|
||||
this.persistMode();
|
||||
@@ -554,10 +564,15 @@ export class ThemeController {
|
||||
const hasValidDefault: boolean = this.isNonEmptyObject(defaultTheme);
|
||||
const hasValidDark: boolean = this.isNonEmptyObject(darkTheme);
|
||||
|
||||
// Check if themes have actual custom tokens (not just empty or algorithm-only)
|
||||
const hasCustomDefault =
|
||||
hasValidDefault && !this.isEmptyTheme(defaultTheme);
|
||||
const hasCustomDark = hasValidDark && !this.isEmptyTheme(darkTheme);
|
||||
|
||||
return {
|
||||
bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null,
|
||||
bootstrapDarkTheme: hasValidDark ? darkTheme : null,
|
||||
hasCustomThemes: hasValidDefault || hasValidDark,
|
||||
bootstrapDefaultTheme: hasCustomDefault ? defaultTheme : null,
|
||||
bootstrapDarkTheme: hasCustomDark ? darkTheme : null,
|
||||
hasCustomThemes: hasCustomDefault || hasCustomDark,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -572,6 +587,20 @@ export class ThemeController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a theme is truly empty (not even an algorithm).
|
||||
* A theme with just an algorithm is still valid and should be used.
|
||||
*/
|
||||
private isEmptyTheme(theme: AnyThemeConfig | undefined): boolean {
|
||||
if (!theme) return true;
|
||||
|
||||
return !(
|
||||
theme.algorithm ||
|
||||
(theme.token && Object.keys(theme.token).length > 0) ||
|
||||
(theme.components && Object.keys(theme.components).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the theme configuration to ensure it has a valid algorithm.
|
||||
* @param theme - The theme configuration to normalize
|
||||
@@ -588,49 +617,45 @@ export class ThemeController {
|
||||
* @returns The theme configuration for the specified mode or null if not available
|
||||
*/
|
||||
private getThemeForMode(mode: ThemeMode): AnyThemeConfig | null {
|
||||
// Priority 1: Dev theme override (highest priority for development)
|
||||
// Dev overrides affect all contexts
|
||||
if (this.devThemeOverride) {
|
||||
return this.devThemeOverride;
|
||||
const normalizedOverride = this.normalizeTheme(this.devThemeOverride);
|
||||
const isDarkMode = isThemeConfigDark(normalizedOverride);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
if (baseTheme) {
|
||||
const mergedTheme = Theme.fromConfig(normalizedOverride, baseTheme);
|
||||
return mergedTheme.toSerializedConfig();
|
||||
}
|
||||
|
||||
return normalizedOverride;
|
||||
}
|
||||
|
||||
// Priority 2: System theme based on mode (applies to all contexts)
|
||||
let resolvedMode: ThemeMode = mode;
|
||||
|
||||
if (mode === ThemeMode.SYSTEM) {
|
||||
// OS preference is allowed when dark theme exists
|
||||
if (this.darkTheme === null) return null;
|
||||
resolvedMode = ThemeController.getSystemPreferredMode();
|
||||
}
|
||||
|
||||
if (!this.hasCustomThemes) {
|
||||
const baseTheme = this.defaultTheme.token as Partial<SupersetTheme>;
|
||||
return getAntdConfig(baseTheme, resolvedMode === ThemeMode.DARK);
|
||||
}
|
||||
if (resolvedMode === ThemeMode.DARK) return this.darkTheme;
|
||||
|
||||
// Handle bootstrap themes using existing normalization
|
||||
const selectedTheme: AnyThemeConfig =
|
||||
resolvedMode === ThemeMode.DARK
|
||||
? this.darkTheme || this.defaultTheme
|
||||
: this.defaultTheme;
|
||||
|
||||
return selectedTheme;
|
||||
return this.defaultTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial theme mode with error recovery.
|
||||
*/
|
||||
private determineInitialMode(): ThemeMode {
|
||||
// Try to restore saved mode first
|
||||
const savedMode: ThemeMode | null = this.loadSavedMode();
|
||||
if (savedMode && this.isValidThemeMode(savedMode)) return savedMode;
|
||||
|
||||
// If no dark theme is available, force default mode
|
||||
if (this.darkTheme === null) {
|
||||
this.storage.removeItem(this.modeStorageKey);
|
||||
return ThemeMode.DEFAULT;
|
||||
}
|
||||
|
||||
// Try to restore saved mode
|
||||
const savedMode: ThemeMode | null = this.loadSavedMode();
|
||||
if (savedMode && this.isValidThemeMode(savedMode)) return savedMode;
|
||||
|
||||
// Default to system preference when both themes are available
|
||||
return ThemeMode.SYSTEM;
|
||||
}
|
||||
@@ -663,11 +688,14 @@ export class ThemeController {
|
||||
// Validate that we have the required theme data for the mode
|
||||
switch (mode) {
|
||||
case ThemeMode.DARK:
|
||||
return !!(this.darkTheme || this.defaultTheme);
|
||||
// Dark mode is valid if we have a dark theme
|
||||
return !!this.darkTheme;
|
||||
case ThemeMode.DEFAULT:
|
||||
// Default mode is valid if we have a default theme
|
||||
return !!this.defaultTheme;
|
||||
case ThemeMode.SYSTEM:
|
||||
return this.darkTheme !== null;
|
||||
// System mode is valid if dark mode is available
|
||||
return !!this.darkTheme;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -698,11 +726,15 @@ export class ThemeController {
|
||||
* Applies the current theme configuration to the global theme.
|
||||
* This method sets the theme on the globalTheme and applies it to the Theme.
|
||||
* It also handles any errors that may occur during the application of the theme.
|
||||
* @param theme - The theme configuration to apply
|
||||
* @param theme - The theme configuration to apply (may already include base theme tokens)
|
||||
*/
|
||||
private applyTheme(theme: AnyThemeConfig): void {
|
||||
try {
|
||||
const normalizedConfig = normalizeThemeConfig(theme);
|
||||
|
||||
// Simply apply the theme - it should already be properly merged if needed
|
||||
// The merging with base theme happens in getThemeForMode() and other methods
|
||||
// that prepare themes before passing them to applyTheme()
|
||||
this.globalTheme.setConfig(normalizedConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply theme:', error);
|
||||
|
||||
Reference in New Issue
Block a user