diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index 271e6890d47c..80dc7d04c8a0 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -154,6 +154,7 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = { }, extra_categorical_color_schemes: [], extra_sequential_color_schemes: [], + extra_theme_tokens: [], theme: { default: {}, dark: {}, diff --git a/superset-frontend/src/theme/utils/antdTokenNames.test.ts b/superset-frontend/src/theme/utils/antdTokenNames.test.ts index cb56d7606ead..de95b6dca608 100644 --- a/superset-frontend/src/theme/utils/antdTokenNames.test.ts +++ b/superset-frontend/src/theme/utils/antdTokenNames.test.ts @@ -22,6 +22,15 @@ import { getAllValidTokenNames, } from './antdTokenNames'; +// Simulate a deployment that registered a custom token via EXTRA_THEME_TOKENS +// (shipped in the common bootstrap payload). +jest.mock('src/utils/getBootstrapData', () => ({ + __esModule: true, + default: () => ({ + common: { extra_theme_tokens: ['customDeploymentToken'] }, + }), +})); + test('isValidTokenName recognizes standard Ant Design tokens', () => { expect(isValidTokenName('colorPrimary')).toBe(true); expect(isValidTokenName('fontSize')).toBe(true); @@ -141,6 +150,20 @@ test('dashboard tile tokens are recognized as valid Superset custom tokens', () }); }); +test('isValidTokenName recognizes deployment-registered EXTRA_THEME_TOKENS', () => { + expect(isValidTokenName('customDeploymentToken')).toBe(true); +}); + +test('isSupersetCustomToken treats EXTRA_THEME_TOKENS as custom, not Ant Design', () => { + expect(isSupersetCustomToken('customDeploymentToken')).toBe(true); +}); + +test('getAllValidTokenNames lists EXTRA_THEME_TOKENS under supersetTokens', () => { + const result = getAllValidTokenNames(); + expect(result.supersetTokens).toContain('customDeploymentToken'); + expect(result.antdTokens).not.toContain('customDeploymentToken'); +}); + test('label variant tokens are recognized as valid Superset custom tokens', () => { const labelTokens = [ // Published/Draft diff --git a/superset-frontend/src/theme/utils/antdTokenNames.ts b/superset-frontend/src/theme/utils/antdTokenNames.ts index 4ca6f54fcf34..a25c6f7c1249 100644 --- a/superset-frontend/src/theme/utils/antdTokenNames.ts +++ b/superset-frontend/src/theme/utils/antdTokenNames.ts @@ -17,6 +17,25 @@ * under the License. */ import { theme } from 'antd'; +import getBootstrapData from 'src/utils/getBootstrapData'; + +/** + * Custom token names registered by the deployment via the EXTRA_THEME_TOKENS + * config (shipped in the common bootstrap payload). Lets deployments and plugins + * validate their own theme tokens without forking this list. + * + * Snapshotted once so the cached valid-token set (isValidTokenName) and the live + * reads (isSupersetCustomToken, getAllValidTokenNames) all derive from the same + * list, rather than depending on getBootstrapData's memoization to stay in sync. + */ +let extraThemeTokensCache: readonly string[] | undefined; +function getExtraThemeTokens(): readonly string[] { + if (extraThemeTokensCache === undefined) { + extraThemeTokensCache = + getBootstrapData()?.common?.extra_theme_tokens ?? []; + } + return extraThemeTokensCache; +} /** * Superset-specific custom tokens that extend Ant Design's token system. @@ -125,10 +144,11 @@ function getValidTokenNames(): Set { const antdTokens = theme.getDesignToken(); const antdTokenNames = Object.keys(antdTokens); - // Combine with Superset custom tokens + // Combine with Superset custom tokens + deployment-registered extras validTokenNamesCache = new Set([ ...antdTokenNames, ...SUPERSET_CUSTOM_TOKENS, + ...getExtraThemeTokens(), ]); } return validTokenNamesCache; @@ -149,7 +169,10 @@ export function isValidTokenName(tokenName: string): boolean { * @returns true if it's a Superset-specific token */ export function isSupersetCustomToken(tokenName: string): boolean { - return SUPERSET_CUSTOM_TOKENS.has(tokenName); + return ( + SUPERSET_CUSTOM_TOKENS.has(tokenName) || + getExtraThemeTokens().includes(tokenName) + ); } /** @@ -165,7 +188,9 @@ export function getAllValidTokenNames(): { const antdTokens = Array.from(allTokens).filter( t => !isSupersetCustomToken(t), ); - const supersetTokens: string[] = Array.from(SUPERSET_CUSTOM_TOKENS); + const supersetTokens: string[] = Array.from( + new Set([...SUPERSET_CUSTOM_TOKENS, ...getExtraThemeTokens()]), + ); return { antdTokens, diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts index a8d195e43d91..9a10e88d684b 100644 --- a/superset-frontend/src/types/bootstrapTypes.ts +++ b/superset-frontend/src/types/bootstrapTypes.ts @@ -168,6 +168,7 @@ export interface CommonBootstrapData { language_pack: LanguagePack; extra_categorical_color_schemes: ColorSchemeConfig[]; extra_sequential_color_schemes: SequentialSchemeConfig[]; + extra_theme_tokens: string[]; theme: BootstrapThemeDataConfig; menu_data: MenuData; d3_format: Partial; diff --git a/superset/config.py b/superset/config.py index 25dac7d6689f..68c81a2392bf 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1063,6 +1063,12 @@ class D3TimeFormat(TypedDict, total=False): # This is merely a default EXTRA_CATEGORICAL_COLOR_SCHEMES: list[dict[str, Any]] = [] +# EXTRA_THEME_TOKENS lets a deployment register additional custom theme token +# names so they validate cleanly in the theme editor instead of being flagged as +# unknown Ant Design tokens. Deployments and plugins that consume their own +# tokens via useTheme add them here. This is merely a default. +EXTRA_THEME_TOKENS: list[str] = [] + # ----------------------------------------------------------------------------- # Theme System Configuration # ----------------------------------------------------------------------------- diff --git a/superset/views/base.py b/superset/views/base.py index a72b9679c872..e4a3d15dfe5e 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -636,6 +636,7 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument "extra_categorical_color_schemes": app.config[ "EXTRA_CATEGORICAL_COLOR_SCHEMES" ], + "extra_theme_tokens": app.config["EXTRA_THEME_TOKENS"], "menu_data": menu_data(g.user), "pdf_compression_level": app.config["PDF_COMPRESSION_LEVEL"], "user_subject_id": _get_user_subject_id(user_id),