mirror of
https://github.com/apache/superset.git
synced 2026-05-05 07:54:17 +00:00
Compare commits
5 Commits
docs/testi
...
theme_dark
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19669a3f91 | ||
|
|
175b696d68 | ||
|
|
7ace5f1737 | ||
|
|
312f6c5ed6 | ||
|
|
79eddd4807 |
@@ -24,6 +24,7 @@ import {
|
|||||||
ClientErrorObject,
|
ClientErrorObject,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import setupErrorMessages from 'src/setup/setupErrorMessages';
|
import setupErrorMessages from 'src/setup/setupErrorMessages';
|
||||||
|
import parseCookie from 'src/utils/parseCookie';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
declare global {
|
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() {
|
export default function setupApp() {
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$(':checkbox[data-checkbox-api-prefix]').change(function (
|
$(':checkbox[data-checkbox-api-prefix]').change(function (
|
||||||
@@ -94,6 +111,9 @@ export default function setupApp() {
|
|||||||
window.$ = $;
|
window.$ = $;
|
||||||
window.jQuery = $;
|
window.jQuery = $;
|
||||||
|
|
||||||
|
// set up the OS-level preference around light/dark mode
|
||||||
|
syncBrowserThemePreferenceWithCookie();
|
||||||
|
|
||||||
// set up app wide custom error messages
|
// set up app wide custom error messages
|
||||||
setupErrorMessages();
|
setupErrorMessages();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
themeObject,
|
themeObject,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { ThemeMode } from '@superset-ui/core/theme/types';
|
import { ThemeMode } from '@superset-ui/core/theme/types';
|
||||||
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
|
|
||||||
export class LocalStorageAdapter implements ThemeStorage {
|
export class LocalStorageAdapter implements ThemeStorage {
|
||||||
getItem(key: string): string | null {
|
getItem(key: string): string | null {
|
||||||
@@ -70,7 +71,13 @@ export class ThemeController {
|
|||||||
this.storageKey = options.storageKey || 'superset-theme';
|
this.storageKey = options.storageKey || 'superset-theme';
|
||||||
this.modeStorageKey = options.modeStorageKey || `${this.storageKey}-mode`;
|
this.modeStorageKey = options.modeStorageKey || `${this.storageKey}-mode`;
|
||||||
this.defaultTheme = options.defaultTheme || {};
|
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
|
// Load customizations from storage
|
||||||
const savedThemeJson = this.storage.getItem(this.storageKey);
|
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 {
|
public destroy(): void {
|
||||||
this.mediaQuery.removeEventListener('change', this.handleSystemThemeChange);
|
this.mediaQuery.removeEventListener('change', this.handleSystemThemeChange);
|
||||||
}
|
}
|
||||||
@@ -138,7 +169,6 @@ export class ThemeController {
|
|||||||
if (!newCustomizations.algorithm) {
|
if (!newCustomizations.algorithm) {
|
||||||
this.currentMode = ThemeMode.LIGHT;
|
this.currentMode = ThemeMode.LIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newCustomizations?.algorithm) {
|
if (newCustomizations?.algorithm) {
|
||||||
this.currentMode = newCustomizations.algorithm as ThemeMode;
|
this.currentMode = newCustomizations.algorithm as ThemeMode;
|
||||||
}
|
}
|
||||||
@@ -184,7 +214,6 @@ export class ThemeController {
|
|||||||
if (this.systemMode === newSystemMode) return;
|
if (this.systemMode === newSystemMode) return;
|
||||||
|
|
||||||
this.systemMode = newSystemMode;
|
this.systemMode = newSystemMode;
|
||||||
// If the current mode is SYSTEM, we need to re-apply the theme
|
|
||||||
if (this.currentMode === ThemeMode.SYSTEM) {
|
if (this.currentMode === ThemeMode.SYSTEM) {
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
@@ -221,7 +250,6 @@ export class ThemeController {
|
|||||||
|
|
||||||
private persist(): void {
|
private persist(): void {
|
||||||
this.storage.setItem(this.modeStorageKey, this.currentMode);
|
this.storage.setItem(this.modeStorageKey, this.currentMode);
|
||||||
|
|
||||||
const { algorithm, ...persistedCustomizations } = this.customizations;
|
const { algorithm, ...persistedCustomizations } = this.customizations;
|
||||||
this.storage.setItem(
|
this.storage.setItem(
|
||||||
this.storageKey,
|
this.storageKey,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TimeLocaleDefinition } from 'd3-time-format';
|
|||||||
import { isPlainObject } from 'lodash';
|
import { isPlainObject } from 'lodash';
|
||||||
import { Languages } from 'src/features/home/LanguagePicker';
|
import { Languages } from 'src/features/home/LanguagePicker';
|
||||||
import type { FlashMessage } from 'src/components';
|
import type { FlashMessage } from 'src/components';
|
||||||
|
import type { SerializableThemeConfig } from '@superset-ui/core/theme/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
@@ -153,7 +154,12 @@ export interface CommonBootstrapData {
|
|||||||
language_pack: LanguagePack;
|
language_pack: LanguagePack;
|
||||||
extra_categorical_color_schemes: ColorSchemeConfig[];
|
extra_categorical_color_schemes: ColorSchemeConfig[];
|
||||||
extra_sequential_color_schemes: SequentialSchemeConfig[];
|
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;
|
menu_data: MenuData;
|
||||||
d3_format: Partial<FormatLocaleDefinition>;
|
d3_format: Partial<FormatLocaleDefinition>;
|
||||||
d3_time_format: Partial<TimeLocaleDefinition>;
|
d3_time_format: Partial<TimeLocaleDefinition>;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Route } from 'react-router-dom';
|
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 { Provider as ReduxProvider } from 'react-redux';
|
||||||
import { QueryParamProvider } from 'use-query-params';
|
import { QueryParamProvider } from 'use-query-params';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
@@ -33,10 +33,7 @@ import '../preamble';
|
|||||||
|
|
||||||
const { common } = getBootstrapData();
|
const { common } = getBootstrapData();
|
||||||
|
|
||||||
if (Object.keys(common?.theme || {}).length > 0) {
|
const themeController = new ThemeController();
|
||||||
themeObject.setConfig(common.theme);
|
|
||||||
}
|
|
||||||
const themeController = new ThemeController({ themeObject });
|
|
||||||
|
|
||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
export const RootContextProviders: React.FC = ({ children }) => {
|
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.
|
# 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
|
# This is intended to use for development, visual review, and theming-debugging
|
||||||
# purposes.
|
# 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
|
# 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.
|
# and see the changes applied to the current theme.
|
||||||
# This is intended to use for theme creation, visual review and theming-debugging
|
# This is intended to use for theme creation, visual review and theming-debugging
|
||||||
# purposes.
|
# 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
|
# Allow users to optionally specify date formats in email subjects, which will
|
||||||
# be parsed if enabled
|
# be parsed if enabled
|
||||||
"DATE_FORMAT_IN_EMAIL_SUBJECT": False,
|
"DATE_FORMAT_IN_EMAIL_SUBJECT": False,
|
||||||
@@ -669,17 +669,17 @@ COMMON_BOOTSTRAP_OVERRIDES_FUNC: Callable[ # noqa: E731
|
|||||||
# This is merely a default
|
# This is merely a default
|
||||||
EXTRA_CATEGORICAL_COLOR_SCHEMES: list[dict[str, Any]] = []
|
EXTRA_CATEGORICAL_COLOR_SCHEMES: list[dict[str, Any]] = []
|
||||||
|
|
||||||
# THEME is used for setting a custom theme to Superset, it follows the ant design
|
# THEME and THEME_DARK are used for setting a custom theme to Superset,
|
||||||
# theme structure
|
# it follows the ant design theme structure.
|
||||||
# You can use the AntDesign theme editor to generate a theme structure
|
# You can use the AntDesign theme editor to generate a theme structure
|
||||||
# https://ant.design/theme-editor
|
# https://ant.design/theme-editor
|
||||||
# To expose a JSON theme editor modal that can be triggered from the navbar
|
# The config are set as a callable returning an antd-compatible theme object
|
||||||
# set the `ENABLE_THEME_EDITOR` feature flag to True.
|
# so that themes can be hot-swapped by fetching a theme object definition remotely
|
||||||
#
|
|
||||||
# To set up the dark theme:
|
|
||||||
# THEME = {"algorithm": "dark"}
|
|
||||||
|
|
||||||
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 is used for adding custom sequential color schemes
|
||||||
# EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
|
# EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from flask import (
|
|||||||
g,
|
g,
|
||||||
get_flashed_messages,
|
get_flashed_messages,
|
||||||
redirect,
|
redirect,
|
||||||
|
request,
|
||||||
Response,
|
Response,
|
||||||
session,
|
session,
|
||||||
url_for,
|
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)
|
@cache_manager.cache.memoize(timeout=60)
|
||||||
def cached_common_bootstrap_data( # pylint: disable=unused-argument
|
def cached_common_bootstrap_data( # pylint: disable=unused-argument
|
||||||
user_id: int | None, locale: Locale | None
|
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(),
|
"feature_flags": get_feature_flags(),
|
||||||
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
|
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
|
||||||
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
|
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
|
||||||
"theme": conf["THEME"],
|
|
||||||
"menu_data": menu_data(g.user),
|
"menu_data": menu_data(g.user),
|
||||||
}
|
}
|
||||||
bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data))
|
bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data))
|
||||||
|
bootstrap_data.update(get_theme_bootstrap_data())
|
||||||
return bootstrap_data
|
return bootstrap_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user