mirror of
https://github.com/apache/superset.git
synced 2026-04-28 12:34:23 +00:00
Compare commits
5 Commits
backup/sem
...
theme_dark
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19669a3f91 | ||
|
|
175b696d68 | ||
|
|
7ace5f1737 | ||
|
|
312f6c5ed6 | ||
|
|
79eddd4807 |
@@ -24,6 +24,7 @@ import {
|
||||
ClientErrorObject,
|
||||
} from '@superset-ui/core';
|
||||
import setupErrorMessages from 'src/setup/setupErrorMessages';
|
||||
import parseCookie from 'src/utils/parseCookie';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
declare global {
|
||||
@@ -59,6 +60,22 @@ function toggleCheckbox(apiUrlPrefix: string, selector: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function syncBrowserThemePreferenceWithCookie() {
|
||||
try {
|
||||
const currentPreference = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
const cookies = parseCookie();
|
||||
|
||||
if (cookies.superset_theme !== currentPreference) {
|
||||
document.cookie = `superset_theme=${currentPreference}; path=/; SameSite=Lax; secure`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to sync theme preference', err);
|
||||
}
|
||||
}
|
||||
|
||||
export default function setupApp() {
|
||||
$(document).ready(function () {
|
||||
$(':checkbox[data-checkbox-api-prefix]').change(function (
|
||||
@@ -94,6 +111,9 @@ export default function setupApp() {
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
|
||||
// set up the OS-level preference around light/dark mode
|
||||
syncBrowserThemePreferenceWithCookie();
|
||||
|
||||
// set up app wide custom error messages
|
||||
setupErrorMessages();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
themeObject,
|
||||
} from '@superset-ui/core';
|
||||
import { ThemeMode } from '@superset-ui/core/theme/types';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
|
||||
export class LocalStorageAdapter implements ThemeStorage {
|
||||
getItem(key: string): string | null {
|
||||
@@ -70,7 +71,13 @@ export class ThemeController {
|
||||
this.storageKey = options.storageKey || 'superset-theme';
|
||||
this.modeStorageKey = options.modeStorageKey || `${this.storageKey}-mode`;
|
||||
this.defaultTheme = options.defaultTheme || {};
|
||||
this.themeObject = options.themeObject;
|
||||
|
||||
// Load themeObject — either passed in or auto-loaded from bootstrap
|
||||
if (options.themeObject) {
|
||||
this.themeObject = options.themeObject;
|
||||
} else {
|
||||
this.themeObject = ThemeController.getThemeFromBootstrapData();
|
||||
}
|
||||
|
||||
// Load customizations from storage
|
||||
const savedThemeJson = this.storage.getItem(this.storageKey);
|
||||
@@ -103,8 +110,32 @@ export class ThemeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up listeners. Should be called when the controller is no longer needed.
|
||||
* Load theme object directly from bootstrapData if not provided explicitly
|
||||
* Note that there is special logic/handling to support getting the first request
|
||||
* where the backend doesn't know about user preferences yet.
|
||||
* In that case, since the backend doesn't know about the user preferences,
|
||||
* it will return both themes to the back as part of the bootstrap data, leaving
|
||||
* the decision to the frontend to pick the right one under `bootstrapData.themes`
|
||||
* once the backend knows about the user preferences, it will return only `bootstrapData.theme`
|
||||
* which takes precedence over `bootstrapData.themes` (not available in this case)
|
||||
*/
|
||||
private static getThemeFromBootstrapData(): Theme {
|
||||
const bootstrapData = getBootstrapData().common;
|
||||
|
||||
let themeConfig: AnyThemeConfig = {};
|
||||
|
||||
if (bootstrapData.theme) {
|
||||
themeConfig = bootstrapData.theme;
|
||||
} else if (bootstrapData.themes) {
|
||||
const systemMode = ThemeController.getSystemMode();
|
||||
const preferred = systemMode;
|
||||
if (bootstrapData.themes[preferred]) {
|
||||
themeConfig = bootstrapData.themes[preferred];
|
||||
}
|
||||
}
|
||||
return Theme.fromConfig(themeConfig);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.mediaQuery.removeEventListener('change', this.handleSystemThemeChange);
|
||||
}
|
||||
@@ -138,7 +169,6 @@ export class ThemeController {
|
||||
if (!newCustomizations.algorithm) {
|
||||
this.currentMode = ThemeMode.LIGHT;
|
||||
}
|
||||
|
||||
if (newCustomizations?.algorithm) {
|
||||
this.currentMode = newCustomizations.algorithm as ThemeMode;
|
||||
}
|
||||
@@ -184,7 +214,6 @@ export class ThemeController {
|
||||
if (this.systemMode === newSystemMode) return;
|
||||
|
||||
this.systemMode = newSystemMode;
|
||||
// If the current mode is SYSTEM, we need to re-apply the theme
|
||||
if (this.currentMode === ThemeMode.SYSTEM) {
|
||||
this.applyTheme();
|
||||
this.notifyListeners();
|
||||
@@ -221,7 +250,6 @@ export class ThemeController {
|
||||
|
||||
private persist(): void {
|
||||
this.storage.setItem(this.modeStorageKey, this.currentMode);
|
||||
|
||||
const { algorithm, ...persistedCustomizations } = this.customizations;
|
||||
this.storage.setItem(
|
||||
this.storageKey,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TimeLocaleDefinition } from 'd3-time-format';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { Languages } from 'src/features/home/LanguagePicker';
|
||||
import type { FlashMessage } from 'src/components';
|
||||
import type { SerializableThemeConfig } from '@superset-ui/core/theme/types';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
@@ -153,7 +154,12 @@ export interface CommonBootstrapData {
|
||||
language_pack: LanguagePack;
|
||||
extra_categorical_color_schemes: ColorSchemeConfig[];
|
||||
extra_sequential_color_schemes: SequentialSchemeConfig[];
|
||||
theme: JsonObject;
|
||||
// Depending on if the cookie is set, either theme or themes will be defined.
|
||||
theme?: SerializableThemeConfig;
|
||||
themes?: {
|
||||
light: SerializableThemeConfig;
|
||||
dark: SerializableThemeConfig;
|
||||
};
|
||||
menu_data: MenuData;
|
||||
d3_format: Partial<FormatLocaleDefinition>;
|
||||
d3_time_format: Partial<TimeLocaleDefinition>;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { Route } from 'react-router-dom';
|
||||
import { getExtensionsRegistry, themeObject } from '@superset-ui/core';
|
||||
import { getExtensionsRegistry } from '@superset-ui/core';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -33,10 +33,7 @@ import '../preamble';
|
||||
|
||||
const { common } = getBootstrapData();
|
||||
|
||||
if (Object.keys(common?.theme || {}).length > 0) {
|
||||
themeObject.setConfig(common.theme);
|
||||
}
|
||||
const themeController = new ThemeController({ themeObject });
|
||||
const themeController = new ThemeController();
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
export const RootContextProviders: React.FC = ({ children }) => {
|
||||
|
||||
@@ -563,12 +563,12 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
|
||||
# Adds a switch to the navbar to easily switch between light and dark themes.
|
||||
# This is intended to use for development, visual review, and theming-debugging
|
||||
# purposes.
|
||||
"THEME_ENABLE_DARK_THEME_SWITCH": False,
|
||||
"THEME_ENABLE_DARK_THEME_SWITCH": True,
|
||||
# Adds a theme editor as a modal dialog in the navbar. Allows people to type in JSON
|
||||
# and see the changes applied to the current theme.
|
||||
# This is intended to use for theme creation, visual review and theming-debugging
|
||||
# purposes.
|
||||
"THEME_ALLOW_THEME_EDITOR_BETA": False,
|
||||
"THEME_ALLOW_THEME_EDITOR_BETA": True,
|
||||
# Allow users to optionally specify date formats in email subjects, which will
|
||||
# be parsed if enabled
|
||||
"DATE_FORMAT_IN_EMAIL_SUBJECT": False,
|
||||
@@ -669,17 +669,17 @@ COMMON_BOOTSTRAP_OVERRIDES_FUNC: Callable[ # noqa: E731
|
||||
# This is merely a default
|
||||
EXTRA_CATEGORICAL_COLOR_SCHEMES: list[dict[str, Any]] = []
|
||||
|
||||
# THEME is used for setting a custom theme to Superset, it follows the ant design
|
||||
# theme structure
|
||||
# THEME and THEME_DARK are used for setting a custom theme to Superset,
|
||||
# it follows the ant design theme structure.
|
||||
# You can use the AntDesign theme editor to generate a theme structure
|
||||
# https://ant.design/theme-editor
|
||||
# To expose a JSON theme editor modal that can be triggered from the navbar
|
||||
# set the `ENABLE_THEME_EDITOR` feature flag to True.
|
||||
#
|
||||
# To set up the dark theme:
|
||||
# THEME = {"algorithm": "dark"}
|
||||
# The config are set as a callable returning an antd-compatible theme object
|
||||
# so that themes can be hot-swapped by fetching a theme object definition remotely
|
||||
|
||||
THEME: dict[str, Any] = {}
|
||||
# Whether to respect the user's OS dark mode setting. If True, THEME_DARK must be set
|
||||
THEME_RESPECT_USER_OS_DARK_SETTING = False
|
||||
THEME: dict[str, Any] = lambda: {} # NOQA
|
||||
THEME_DARK: dict[str, Any] = lambda: {"algorithm": "dark"} # NOQA
|
||||
|
||||
# EXTRA_SEQUENTIAL_COLOR_SCHEMES is used for adding custom sequential color schemes
|
||||
# EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
|
||||
|
||||
@@ -30,6 +30,7 @@ from flask import (
|
||||
g,
|
||||
get_flashed_messages,
|
||||
redirect,
|
||||
request,
|
||||
Response,
|
||||
session,
|
||||
url_for,
|
||||
@@ -292,6 +293,27 @@ def menu_data(user: User) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def get_theme_bootstrap_data() -> dict[str, Any]:
|
||||
"""
|
||||
On frst call, the cookie related to light/dark may not be set,
|
||||
so we send both the dark and light themes
|
||||
in the "themes" key. Once the cookie is set, we simply use the `theme` key.
|
||||
Logic on the frontend looks for `theme`, and if it's not available, falls back
|
||||
to `themes` and picks the right one
|
||||
"""
|
||||
theme_cookie = request.cookies.get("superset_theme")
|
||||
if conf["THEME_RESPECT_USER_OS_DARK_SETTING"] or theme_cookie == "light":
|
||||
return {"theme": conf["THEME"]()}
|
||||
elif theme_cookie == "dark":
|
||||
return {"theme": conf["THEME_DARK"]()}
|
||||
return {
|
||||
"themes": {
|
||||
"light": conf["THEME"](),
|
||||
"dark": conf["THEME_DARK"](),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@cache_manager.cache.memoize(timeout=60)
|
||||
def cached_common_bootstrap_data( # pylint: disable=unused-argument
|
||||
user_id: int | None, locale: Locale | None
|
||||
@@ -371,10 +393,10 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument
|
||||
"feature_flags": get_feature_flags(),
|
||||
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
|
||||
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
|
||||
"theme": conf["THEME"],
|
||||
"menu_data": menu_data(g.user),
|
||||
}
|
||||
bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data))
|
||||
bootstrap_data.update(get_theme_bootstrap_data())
|
||||
return bootstrap_data
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user