This is an automated email from the ASF dual-hosted git repository. elizabeth pushed a commit to branch elizabeth/fix-resize-bug in repository https://gitbox.apache.org/repos/asf/superset.git
commit cfd6d8e31deb4aaedc79f9f01ced83970e3947d1 Author: Gabriel Torres Ruiz <[email protected]> AuthorDate: Mon Jul 28 23:26:17 2025 -0300 feat(theming): Align embedded sdk with theme configs (#34273) --- .../TelemetryPixel/TelemetryPixel.test.tsx | 2 +- .../src/components/TelemetryPixel/index.tsx | 3 +- .../src/components/ThemeSelect/index.tsx | 116 -------- .../components/ThemeSubMenu/ThemeSubMenu.test.tsx | 273 ++++++++++++++++++ .../src/components/ThemeSubMenu/index.tsx | 170 ++++++++++++ .../superset-ui-core/src/components/index.ts | 2 + .../packages/superset-ui-core/src/theme/index.tsx | 14 +- .../packages/superset-ui-core/src/theme/types.ts | 13 + .../src/embedded/EmbeddedContextProviders.tsx | 93 +++++++ superset-frontend/src/embedded/index.tsx | 40 ++- superset-frontend/src/features/home/RightMenu.tsx | 36 +-- superset-frontend/src/theme/ThemeController.ts | 74 +++-- superset-frontend/src/theme/ThemeProvider.tsx | 8 +- .../src/theme/tests/ThemeController.test.ts | 309 ++++++++++++++++++++- .../src/theme/tests/ThemeProvider.test.tsx | 3 +- superset-frontend/src/types/bootstrapTypes.ts | 18 +- 16 files changed, 988 insertions(+), 186 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx index 7d12a612d7..7dc5d4e63d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import { render } from '@superset-ui/core/spec'; -import TelemetryPixel from '.'; +import { TelemetryPixel } from '.'; const OLD_ENV = process.env; diff --git a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx index 9e7818a1b7..d7c86a492e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx @@ -39,7 +39,7 @@ interface TelemetryPixelProps { const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0'; -const TelemetryPixel = ({ +export const TelemetryPixel = ({ version = 'unknownVersion', sha = 'unknownSHA', build = 'unknownBuild', @@ -56,4 +56,3 @@ const TelemetryPixel = ({ /> ); }; -export default TelemetryPixel; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx deleted file mode 100644 index e46a31614c..0000000000 --- a/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Dropdown, Icons } from '@superset-ui/core/components'; -import type { MenuItem } from '@superset-ui/core/components/Menu'; -import { t, useTheme } from '@superset-ui/core'; -import { ThemeAlgorithm, ThemeMode } from '../../theme/types'; - -export interface ThemeSelectProps { - setThemeMode: (newMode: ThemeMode) => void; - tooltipTitle?: string; - themeMode: ThemeMode; - hasLocalOverride?: boolean; - onClearLocalSettings?: () => void; - allowOSPreference?: boolean; -} - -const ThemeSelect: React.FC<ThemeSelectProps> = ({ - setThemeMode, - tooltipTitle = 'Select theme', - themeMode, - hasLocalOverride = false, - onClearLocalSettings, - allowOSPreference = true, -}) => { - const theme = useTheme(); - - const handleSelect = (mode: ThemeMode) => { - setThemeMode(mode); - }; - - const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = { - [ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />, - [ThemeAlgorithm.DARK]: <Icons.MoonOutlined />, - [ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />, - [ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />, - }; - - // Use different icon when local theme is active - const triggerIcon = hasLocalOverride ? ( - <Icons.FormatPainterOutlined style={{ color: theme.colorErrorText }} /> - ) : ( - themeIconMap[themeMode] || <Icons.FormatPainterOutlined /> - ); - - const menuItems: MenuItem[] = [ - { - type: 'group', - label: t('Theme'), - }, - { - key: ThemeMode.DEFAULT, - label: t('Light'), - icon: <Icons.SunOutlined />, - onClick: () => handleSelect(ThemeMode.DEFAULT), - }, - { - key: ThemeMode.DARK, - label: t('Dark'), - icon: <Icons.MoonOutlined />, - onClick: () => handleSelect(ThemeMode.DARK), - }, - ...(allowOSPreference - ? [ - { - key: ThemeMode.SYSTEM, - label: t('Match system'), - icon: <Icons.FormatPainterOutlined />, - onClick: () => handleSelect(ThemeMode.SYSTEM), - }, - ] - : []), - ]; - - // Add clear settings option only when there's a local theme active - if (onClearLocalSettings && hasLocalOverride) { - menuItems.push( - { type: 'divider' } as MenuItem, - { - key: 'clear-local', - label: t('Clear local theme'), - icon: <Icons.ClearOutlined />, - onClick: onClearLocalSettings, - } as MenuItem, - ); - } - - return ( - <Dropdown - menu={{ - items: menuItems, - selectedKeys: [themeMode], - }} - trigger={['hover']} - > - {triggerIcon} - </Dropdown> - ); -}; - -export default ThemeSelect; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx new file mode 100644 index 0000000000..b806049e4d --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx @@ -0,0 +1,273 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + render, + screen, + userEvent, + waitFor, + within, +} from '@superset-ui/core/spec'; +import { ThemeMode } from '@superset-ui/core'; +import { Menu } from '@superset-ui/core/components'; +import { ThemeSubMenu } from '.'; + +// Mock the translation function +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + t: (key: string) => key, +})); + +describe('ThemeSubMenu', () => { + const defaultProps = { + allowOSPreference: true, + setThemeMode: jest.fn(), + themeMode: ThemeMode.DEFAULT, + hasLocalOverride: false, + onClearLocalSettings: jest.fn(), + }; + + const renderThemeSubMenu = (props = defaultProps) => + render( + <Menu> + <ThemeSubMenu {...props} /> + </Menu>, + ); + + const findMenuWithText = async (text: string) => { + await waitFor(() => { + const found = screen + .getAllByRole('menu') + .some(m => within(m).queryByText(text)); + + if (!found) throw new Error(`Menu with text "${text}" not yet rendered`); + }); + + return screen.getAllByRole('menu').find(m => within(m).queryByText(text))!; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders Light and Dark theme options by default', async () => { + renderThemeSubMenu(); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Light'); + + expect(within(menu!).getByText('Light')).toBeInTheDocument(); + expect(within(menu!).getByText('Dark')).toBeInTheDocument(); + }); + + it('does not render Match system option when allowOSPreference is false', async () => { + renderThemeSubMenu({ ...defaultProps, allowOSPreference: false }); + userEvent.hover(await screen.findByRole('menuitem')); + + await waitFor(() => { + expect(screen.queryByText('Match system')).not.toBeInTheDocument(); + }); + }); + + it('renders with allowOSPreference as true by default', async () => { + renderThemeSubMenu(); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Match system'); + + expect(within(menu).getByText('Match system')).toBeInTheDocument(); + }); + + it('renders clear option when both hasLocalOverride and onClearLocalSettings are provided', async () => { + const mockClear = jest.fn(); + renderThemeSubMenu({ + ...defaultProps, + hasLocalOverride: true, + onClearLocalSettings: mockClear, + }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Clear local theme'); + + expect(within(menu).getByText('Clear local theme')).toBeInTheDocument(); + }); + + it('does not render clear option when hasLocalOverride is false', async () => { + const mockClear = jest.fn(); + renderThemeSubMenu({ + ...defaultProps, + hasLocalOverride: false, + onClearLocalSettings: mockClear, + }); + + userEvent.hover(await screen.findByRole('menuitem')); + + await waitFor(() => { + expect(screen.queryByText('Clear local theme')).not.toBeInTheDocument(); + }); + }); + + it('calls setThemeMode with DEFAULT when Light is clicked', async () => { + const mockSet = jest.fn(); + renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Light'); + userEvent.click(within(menu).getByText('Light')); + + expect(mockSet).toHaveBeenCalledWith(ThemeMode.DEFAULT); + }); + + it('calls setThemeMode with DARK when Dark is clicked', async () => { + const mockSet = jest.fn(); + renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Dark'); + userEvent.click(within(menu).getByText('Dark')); + + expect(mockSet).toHaveBeenCalledWith(ThemeMode.DARK); + }); + + it('calls setThemeMode with SYSTEM when Match system is clicked', async () => { + const mockSet = jest.fn(); + renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Match system'); + userEvent.click(within(menu).getByText('Match system')); + + expect(mockSet).toHaveBeenCalledWith(ThemeMode.SYSTEM); + }); + + it('calls onClearLocalSettings when Clear local theme is clicked', async () => { + const mockClear = jest.fn(); + renderThemeSubMenu({ + ...defaultProps, + hasLocalOverride: true, + onClearLocalSettings: mockClear, + }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Clear local theme'); + userEvent.click(within(menu).getByText('Clear local theme')); + + expect(mockClear).toHaveBeenCalledTimes(1); + }); + + it('displays sun icon for DEFAULT theme', () => { + renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DEFAULT }); + expect(screen.getByTestId('sun')).toBeInTheDocument(); + }); + + it('displays moon icon for DARK theme', () => { + renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DARK }); + expect(screen.getByTestId('moon')).toBeInTheDocument(); + }); + + it('displays format-painter icon for SYSTEM theme', () => { + renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.SYSTEM }); + expect(screen.getByTestId('format-painter')).toBeInTheDocument(); + }); + + it('displays override icon when hasLocalOverride is true', () => { + renderThemeSubMenu({ ...defaultProps, hasLocalOverride: true }); + expect(screen.getByTestId('format-painter')).toBeInTheDocument(); + }); + + it('renders Theme group header', async () => { + renderThemeSubMenu(); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Theme'); + + expect(within(menu).getByText('Theme')).toBeInTheDocument(); + }); + + it('renders sun icon for Light theme option', async () => { + renderThemeSubMenu(); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Light'); + const lightOption = within(menu).getByText('Light').closest('li'); + + expect(within(lightOption!).getByTestId('sun')).toBeInTheDocument(); + }); + + it('renders moon icon for Dark theme option', async () => { + renderThemeSubMenu(); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Dark'); + const darkOption = within(menu).getByText('Dark').closest('li'); + + expect(within(darkOption!).getByTestId('moon')).toBeInTheDocument(); + }); + + it('renders format-painter icon for Match system option', async () => { + renderThemeSubMenu({ ...defaultProps, allowOSPreference: true }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Match system'); + const matchOption = within(menu).getByText('Match system').closest('li'); + + expect( + within(matchOption!).getByTestId('format-painter'), + ).toBeInTheDocument(); + }); + + it('renders clear icon for Clear local theme option', async () => { + renderThemeSubMenu({ + ...defaultProps, + hasLocalOverride: true, + onClearLocalSettings: jest.fn(), + }); + + userEvent.hover(await screen.findByRole('menuitem')); + const menu = await findMenuWithText('Clear local theme'); + const clearOption = within(menu) + .getByText('Clear local theme') + .closest('li'); + + expect(within(clearOption!).getByTestId('clear')).toBeInTheDocument(); + }); + + it('renders divider before clear option when clear option is present', async () => { + renderThemeSubMenu({ + ...defaultProps, + hasLocalOverride: true, + onClearLocalSettings: jest.fn(), + }); + + userEvent.hover(await screen.findByRole('menuitem')); + + const menu = await findMenuWithText('Clear local theme'); + const divider = within(menu).queryByRole('separator'); + + expect(divider).toBeInTheDocument(); + }); + + it('does not render divider when clear option is not present', async () => { + renderThemeSubMenu({ ...defaultProps }); + + userEvent.hover(await screen.findByRole('menuitem')); + const divider = document.querySelector('.ant-menu-item-divider'); + + expect(divider).toBeNull(); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx new file mode 100644 index 0000000000..2dbb36104b --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx @@ -0,0 +1,170 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useMemo } from 'react'; +import { Icons, Menu } from '@superset-ui/core/components'; +import { + css, + styled, + t, + ThemeMode, + useTheme, + ThemeAlgorithm, +} from '@superset-ui/core'; + +const StyledThemeSubMenu = styled(Menu.SubMenu)` + ${({ theme }) => css` + [data-icon='caret-down'] { + color: ${theme.colorIcon}; + font-size: ${theme.fontSizeXS}px; + margin-left: ${theme.sizeUnit}px; + } + &.ant-menu-submenu-active { + .ant-menu-title-content { + color: ${theme.colorPrimary}; + } + } + `} +`; + +const StyledThemeSubMenuItem = styled(Menu.Item)<{ selected: boolean }>` + ${({ theme, selected }) => css` + &:hover { + color: ${theme.colorPrimary} !important; + cursor: pointer !important; + } + ${selected && + css` + background-color: ${theme.colors.primary.light4} !important; + color: ${theme.colors.primary.dark1} !important; + `} + `} +`; + +export interface ThemeSubMenuOption { + key: ThemeMode; + label: string; + icon: React.ReactNode; + onClick: () => void; +} + +export interface ThemeSubMenuProps { + setThemeMode: (newMode: ThemeMode) => void; + themeMode: ThemeMode; + hasLocalOverride?: boolean; + onClearLocalSettings?: () => void; + allowOSPreference?: boolean; +} + +export const ThemeSubMenu: React.FC<ThemeSubMenuProps> = ({ + setThemeMode, + themeMode, + hasLocalOverride = false, + onClearLocalSettings, + allowOSPreference = true, +}: ThemeSubMenuProps) => { + const theme = useTheme(); + + const handleSelect = (mode: ThemeMode) => { + setThemeMode(mode); + }; + + const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = + useMemo( + () => ({ + [ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />, + [ThemeAlgorithm.DARK]: <Icons.MoonOutlined />, + [ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />, + [ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />, + }), + [], + ); + + const selectedThemeModeIcon = useMemo( + () => + hasLocalOverride ? ( + <Icons.FormatPainterOutlined + style={{ color: theme.colors.error.base }} + /> + ) : ( + themeIconMap[themeMode] + ), + [hasLocalOverride, theme.colors.error.base, themeIconMap, themeMode], + ); + + const themeOptions: ThemeSubMenuOption[] = [ + { + key: ThemeMode.DEFAULT, + label: t('Light'), + icon: <Icons.SunOutlined />, + onClick: () => handleSelect(ThemeMode.DEFAULT), + }, + { + key: ThemeMode.DARK, + label: t('Dark'), + icon: <Icons.MoonOutlined />, + onClick: () => handleSelect(ThemeMode.DARK), + }, + ...(allowOSPreference + ? [ + { + key: ThemeMode.SYSTEM, + label: t('Match system'), + icon: <Icons.FormatPainterOutlined />, + onClick: () => handleSelect(ThemeMode.SYSTEM), + }, + ] + : []), + ]; + + // Add clear settings option only when there's a local theme active + const clearOption = + onClearLocalSettings && hasLocalOverride + ? { + key: 'clear-local', + label: t('Clear local theme'), + icon: <Icons.ClearOutlined />, + onClick: onClearLocalSettings, + } + : null; + + return ( + <StyledThemeSubMenu + key="theme-sub-menu" + title={selectedThemeModeIcon} + icon={<Icons.CaretDownOutlined iconSize="xs" />} + > + <Menu.ItemGroup title={t('Theme')} /> + {themeOptions.map(option => ( + <StyledThemeSubMenuItem + key={option.key} + onClick={option.onClick} + selected={option.key === themeMode} + > + {option.icon} {option.label} + </StyledThemeSubMenuItem> + ))} + {clearOption && [ + <Menu.Divider key="theme-divider" />, + <Menu.Item key={clearOption.key} onClick={clearOption.onClick}> + {clearOption.icon} {clearOption.label} + </Menu.Item>, + ]} + </StyledThemeSubMenu> + ); +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts index e149906997..0053d50164 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts @@ -164,6 +164,8 @@ export * from './Steps'; export * from './Table'; export * from './TableView'; export * from './Tag'; +export * from './TelemetryPixel'; +export * from './ThemeSubMenu'; export * from './UnsavedChangesModal'; export * from './constants'; export * from './Result'; diff --git a/superset-frontend/packages/superset-ui-core/src/theme/index.tsx b/superset-frontend/packages/superset-ui-core/src/theme/index.tsx index 26c208ad12..8b788f2e9c 100644 --- a/superset-frontend/packages/superset-ui-core/src/theme/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/theme/index.tsx @@ -26,7 +26,9 @@ import { type ThemeStorage, type ThemeControllerOptions, type ThemeContextType, + type SupersetThemeConfig, ThemeAlgorithm, + ThemeMode, } from './types'; export { @@ -66,7 +68,16 @@ const themeObject: Theme = Theme.fromConfig({ const { theme } = themeObject; const supersetTheme = theme; -export { Theme, themeObject, styled, theme, supersetTheme }; +export { + Theme, + ThemeAlgorithm, + ThemeMode, + themeObject, + styled, + theme, + supersetTheme, +}; + export type { SupersetTheme, SerializableThemeConfig, @@ -74,6 +85,7 @@ export type { ThemeStorage, ThemeControllerOptions, ThemeContextType, + SupersetThemeConfig, }; // Export theme utility functions diff --git a/superset-frontend/packages/superset-ui-core/src/theme/types.ts b/superset-frontend/packages/superset-ui-core/src/theme/types.ts index 41011eb73f..b3cf0ae411 100644 --- a/superset-frontend/packages/superset-ui-core/src/theme/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/theme/types.ts @@ -429,3 +429,16 @@ export interface ThemeContextType { canDetectOSPreference: () => boolean; createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>; } + +/** + * Configuration object for complete theme setup including default, dark themes and settings + */ +export interface SupersetThemeConfig { + theme_default: AnyThemeConfig; + theme_dark?: AnyThemeConfig; + theme_settings?: { + enforced?: boolean; + allowSwitching?: boolean; + allowOSPreference?: boolean; + }; +} diff --git a/superset-frontend/src/embedded/EmbeddedContextProviders.tsx b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx new file mode 100644 index 0000000000..207a1d320d --- /dev/null +++ b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx @@ -0,0 +1,93 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Route } from 'react-router-dom'; +import { getExtensionsRegistry } from '@superset-ui/core'; +import { Provider as ReduxProvider } from 'react-redux'; +import { QueryParamProvider } from 'use-query-params'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { FlashProvider, DynamicPluginProvider } from 'src/components'; +import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext'; +import { SupersetThemeProvider } from 'src/theme/ThemeProvider'; +import { ThemeController } from 'src/theme/ThemeController'; +import type { ThemeStorage } from '@superset-ui/core'; +import { store } from 'src/views/store'; +import getBootstrapData from 'src/utils/getBootstrapData'; + +/** + * In-memory implementation of ThemeStorage interface for embedded contexts. + * Persistent storage is not required for embedded dashboards. + */ +class ThemeMemoryStorageAdapter implements ThemeStorage { + private storage = new Map<string, string>(); + + getItem(key: string): string | null { + return this.storage.get(key) || null; + } + + setItem(key: string, value: string): void { + this.storage.set(key, value); + } + + removeItem(key: string): void { + this.storage.delete(key); + } +} + +const themeController = new ThemeController({ + storage: new ThemeMemoryStorageAdapter(), +}); + +export const getThemeController = (): ThemeController => themeController; + +const { common } = getBootstrapData(); +const extensionsRegistry = getExtensionsRegistry(); + +export const EmbeddedContextProviders: React.FC = ({ children }) => { + const RootContextProviderExtension = extensionsRegistry.get( + 'root.context.provider', + ); + + return ( + <SupersetThemeProvider themeController={themeController}> + <ReduxProvider store={store}> + <DndProvider backend={HTML5Backend}> + <FlashProvider messages={common.flash_messages}> + <EmbeddedUiConfigProvider> + <DynamicPluginProvider> + <QueryParamProvider + ReactRouterRoute={Route} + stringifyOptions={{ encode: false }} + > + {RootContextProviderExtension ? ( + <RootContextProviderExtension> + {children} + </RootContextProviderExtension> + ) : ( + children + )} + </QueryParamProvider> + </DynamicPluginProvider> + </EmbeddedUiConfigProvider> + </FlashProvider> + </DndProvider> + </ReduxProvider> + </SupersetThemeProvider> + ); +}; diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx index c577015748..c207524961 100644 --- a/superset-frontend/src/embedded/index.tsx +++ b/superset-frontend/src/embedded/index.tsx @@ -21,20 +21,27 @@ import 'src/public-path'; import { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom'; -import { makeApi, t, logging, themeObject } from '@superset-ui/core'; +import { + type SupersetThemeConfig, + makeApi, + t, + logging, +} from '@superset-ui/core'; import Switchboard from '@superset-ui/switchboard'; import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData'; import setupClient from 'src/setup/setupClient'; import setupPlugins from 'src/setup/setupPlugins'; import { useUiConfig } from 'src/components/UiConfigContext'; -import { RootContextProviders } from 'src/views/RootContextProviders'; import { store, USER_LOADED } from 'src/views/store'; import { Loading } from '@superset-ui/core/components'; import { ErrorBoundary } from 'src/components'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; -import { AnyThemeConfig } from 'packages/superset-ui-core/src/theme/types'; +import { + EmbeddedContextProviders, + getThemeController, +} from './EmbeddedContextProviders'; import { embeddedApi } from './api'; import { getDataMaskChangeTrigger } from './utils'; @@ -44,9 +51,7 @@ const debugMode = process.env.WEBPACK_MODE === 'development'; const bootstrapData = getBootstrapData(); function log(...info: unknown[]) { - if (debugMode) { - logging.debug(`[superset]`, ...info); - } + if (debugMode) logging.debug(`[superset]`, ...info); } const LazyDashboardPage = lazy( @@ -85,12 +90,12 @@ const EmbededLazyDashboardPage = () => { const EmbeddedRoute = () => ( <Suspense fallback={<Loading />}> - <RootContextProviders> + <EmbeddedContextProviders> <ErrorBoundary> <EmbededLazyDashboardPage /> </ErrorBoundary> <ToastContainer position="top" /> - </RootContextProviders> + </EmbeddedContextProviders> </Suspense> ); @@ -245,12 +250,13 @@ window.addEventListener('message', function embeddedPageInitializer(event) { Switchboard.defineMethod('getDataMask', embeddedApi.getDataMask); Switchboard.defineMethod( 'setThemeConfig', - (payload: { themeConfig: AnyThemeConfig }) => { + (payload: { themeConfig: SupersetThemeConfig }) => { const { themeConfig } = payload; log('Received setThemeConfig request:', themeConfig); try { - themeObject.setConfig(themeConfig); + const themeController = getThemeController(); + themeController.setThemeConfig(themeConfig); return { success: true, message: 'Theme applied' }; } catch (error) { logging.error('Failed to apply theme config:', error); @@ -258,8 +264,22 @@ window.addEventListener('message', function embeddedPageInitializer(event) { } }, ); + Switchboard.start(); } }); +// Clean up theme controller on page unload +window.addEventListener('beforeunload', () => { + try { + const controller = getThemeController(); + if (controller) { + log('Destroying theme controller'); + controller.destroy(); + } + } catch (error) { + logging.warn('Failed to destroy theme controller:', error); + } +}); + log('embed page is ready to receive messages'); diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index 2106ab3f53..c9573ab81d 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -17,13 +17,11 @@ * under the License. */ import { Fragment, useState, useEffect, FC, PureComponent } from 'react'; - import rison from 'rison'; import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { useQueryParams, BooleanParam } from 'use-query-params'; import { get, isEmpty } from 'lodash'; - import { t, styled, @@ -33,10 +31,15 @@ import { getExtensionsRegistry, useTheme, } from '@superset-ui/core'; -import { Menu } from '@superset-ui/core/components/Menu'; -import { Label, Tooltip } from '@superset-ui/core/components'; -import { Icons } from '@superset-ui/core/components/Icons'; -import { Typography } from '@superset-ui/core/components/Typography'; +import { + Label, + Tooltip, + ThemeSubMenu, + Menu, + Icons, + Typography, + TelemetryPixel, +} from '@superset-ui/core/components'; import { ensureAppRoot } from 'src/utils/pathUtils'; import { findPermission } from 'src/utils/findPermission'; import { isUserAdmin } from 'src/dashboard/util/permissionUtils'; @@ -49,9 +52,7 @@ import { RootState } from 'src/dashboard/types'; import DatabaseModal from 'src/features/databases/DatabaseModal'; import UploadDataModal from 'src/features/databases/UploadDataModel'; import { uploadUserPerms } from 'src/views/CRUD/utils'; -import TelemetryPixel from '@superset-ui/core/components/TelemetryPixel'; import { useThemeContext } from 'src/theme/ThemeProvider'; -import ThemeSelect from '@superset-ui/core/components/ThemeSelect'; import LanguagePicker from './LanguagePicker'; import { ExtensionConfigs, @@ -138,6 +139,7 @@ const RightMenu = ({ datasetAdded?: boolean; }) => void; }) => { + const theme = useTheme(); const user = useSelector<any, UserWithPermissionsAndRoles>( state => state.user, ); @@ -371,7 +373,6 @@ const RightMenu = ({ localStorage.removeItem('redux'); }; - const theme = useTheme(); return ( <StyledDiv align={align}> {canDatabase && ( @@ -493,16 +494,15 @@ const RightMenu = ({ })} </StyledSubMenu> )} + {canSetMode() && ( - <span> - <ThemeSelect - setThemeMode={setThemeMode} - themeMode={themeMode} - hasLocalOverride={hasDevOverride()} - onClearLocalSettings={clearLocalOverrides} - allowOSPreference={canDetectOSPreference()} - /> - </span> + <ThemeSubMenu + setThemeMode={setThemeMode} + themeMode={themeMode} + hasLocalOverride={hasDevOverride()} + onClearLocalSettings={clearLocalOverrides} + allowOSPreference={canDetectOSPreference()} + /> )} <StyledSubMenu diff --git a/superset-frontend/src/theme/ThemeController.ts b/superset-frontend/src/theme/ThemeController.ts index 3f33894088..5a1851e8f5 100644 --- a/superset-frontend/src/theme/ThemeController.ts +++ b/superset-frontend/src/theme/ThemeController.ts @@ -17,13 +17,15 @@ * under the License. */ import { + type AnyThemeConfig, + type SupersetTheme, + type SupersetThemeConfig, + type ThemeControllerOptions, + type ThemeStorage, Theme, - AnyThemeConfig, - ThemeStorage, - ThemeControllerOptions, + ThemeMode, themeObject as supersetThemeObject, } from '@superset-ui/core'; -import { SupersetTheme, ThemeMode } from '@superset-ui/core/theme/types'; import { getAntdConfig, normalizeThemeConfig, @@ -94,7 +96,7 @@ export class ThemeController { private currentMode: ThemeMode; - private readonly hasBootstrapThemes: boolean; + private hasCustomThemes: boolean; private onChangeCallbacks: Set<(theme: Theme) => void> = new Set(); @@ -109,15 +111,13 @@ export class ThemeController { private dashboardCrudTheme: AnyThemeConfig | null = null; - constructor(options: ThemeControllerOptions = {}) { - const { - storage = new LocalStorageAdapter(), - modeStorageKey = STORAGE_KEYS.THEME_MODE, - themeObject = supersetThemeObject, - defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {}, - onChange = null, - } = options; - + constructor({ + storage = new LocalStorageAdapter(), + modeStorageKey = STORAGE_KEYS.THEME_MODE, + themeObject = supersetThemeObject, + defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {}, + onChange = undefined, + }: ThemeControllerOptions = {}) { this.storage = storage; this.modeStorageKey = modeStorageKey; @@ -129,14 +129,14 @@ export class ThemeController { bootstrapDefaultTheme, bootstrapDarkTheme, bootstrapThemeSettings, - hasBootstrapThemes, + hasCustomThemes, }: BootstrapThemeData = this.loadBootstrapData(); - this.hasBootstrapThemes = hasBootstrapThemes; + this.hasCustomThemes = hasCustomThemes; this.themeSettings = bootstrapThemeSettings || {}; // Set themes based on bootstrap data availability - if (this.hasBootstrapThemes) { + if (this.hasCustomThemes) { this.darkTheme = bootstrapDarkTheme || bootstrapDefaultTheme || null; this.defaultTheme = bootstrapDefaultTheme || bootstrapDarkTheme || defaultTheme; @@ -424,6 +424,42 @@ export class ThemeController { return allowOSPreference === true; } + /** + * Sets an entire new theme configuration, replacing all existing theme data and settings. + * This method is designed for use cases like embedded dashboards where themes are provided + * dynamically from external sources. + * @param config - The complete theme configuration object + */ + public setThemeConfig(config: SupersetThemeConfig): void { + this.defaultTheme = config.theme_default; + this.darkTheme = config.theme_dark || null; + this.hasCustomThemes = true; + + this.themeSettings = { + enforced: config.theme_settings?.enforced ?? false, + allowSwitching: config.theme_settings?.allowSwitching ?? true, + allowOSPreference: config.theme_settings?.allowOSPreference ?? true, + }; + + let newMode: ThemeMode; + try { + this.validateModeUpdatePermission(this.currentMode); + const hasRequiredTheme = this.isValidThemeMode(this.currentMode); + newMode = hasRequiredTheme + ? this.currentMode + : this.determineInitialMode(); + } catch { + newMode = this.determineInitialMode(); + } + + this.currentMode = newMode; + + const themeToApply = + this.getThemeForMode(this.currentMode) || this.defaultTheme; + + this.updateTheme(themeToApply); + } + /** * Handles system theme changes with error recovery. */ @@ -547,7 +583,7 @@ export class ThemeController { bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null, bootstrapDarkTheme: hasValidDark ? darkTheme : null, bootstrapThemeSettings: hasValidSettings ? themeSettings : null, - hasBootstrapThemes: hasValidDefault || hasValidDark, + hasCustomThemes: hasValidDefault || hasValidDark, }; } @@ -607,7 +643,7 @@ export class ThemeController { resolvedMode = ThemeController.getSystemPreferredMode(); } - if (!this.hasBootstrapThemes) { + if (!this.hasCustomThemes) { const baseTheme = this.defaultTheme.token as Partial<SupersetTheme>; return getAntdConfig(baseTheme, resolvedMode === ThemeMode.DARK); } diff --git a/superset-frontend/src/theme/ThemeProvider.tsx b/superset-frontend/src/theme/ThemeProvider.tsx index 5b4b64c799..2ec902889b 100644 --- a/superset-frontend/src/theme/ThemeProvider.tsx +++ b/superset-frontend/src/theme/ThemeProvider.tsx @@ -24,8 +24,12 @@ import { useMemo, useState, } from 'react'; -import { Theme, AnyThemeConfig, ThemeContextType } from '@superset-ui/core'; -import { ThemeMode } from '@superset-ui/core/theme/types'; +import { + type AnyThemeConfig, + type ThemeContextType, + Theme, + ThemeMode, +} from '@superset-ui/core'; import { ThemeController } from './ThemeController'; const ThemeContext = createContext<ThemeContextType | null>(null); diff --git a/superset-frontend/src/theme/tests/ThemeController.test.ts b/superset-frontend/src/theme/tests/ThemeController.test.ts index cc242e849d..1851dfabd9 100644 --- a/superset-frontend/src/theme/tests/ThemeController.test.ts +++ b/superset-frontend/src/theme/tests/ThemeController.test.ts @@ -17,12 +17,17 @@ * under the License. */ import { theme as antdThemeImport } from 'antd'; -import { Theme } from '@superset-ui/core'; +import { + type AnyThemeConfig, + type SupersetThemeConfig, + Theme, + ThemeAlgorithm, + ThemeMode, +} from '@superset-ui/core'; import type { BootstrapThemeDataConfig, CommonBootstrapData, } from 'src/types/bootstrapTypes'; -import { ThemeAlgorithm, ThemeMode } from '@superset-ui/core/theme/types'; import getBootstrapData from 'src/utils/getBootstrapData'; import { LocalStorageAdapter, ThemeController } from '../ThemeController'; @@ -43,7 +48,7 @@ const mockThemeFromConfig = jest.fn(); const mockSetConfig = jest.fn(); // Mock data constants -const DEFAULT_THEME = { +const DEFAULT_THEME: AnyThemeConfig = { token: { colorBgBase: '#ededed', colorTextBase: '#120f0f', @@ -55,7 +60,7 @@ const DEFAULT_THEME = { }, }; -const DARK_THEME = { +const DARK_THEME: AnyThemeConfig = { token: { colorBgBase: '#141118', colorTextBase: '#fdc7c7', @@ -65,7 +70,7 @@ const DARK_THEME = { colorSuccess: '#3c7c1b', colorWarning: '#dc9811', }, - algorithm: ThemeMode.DARK, + algorithm: ThemeAlgorithm.DARK, }; const THEME_SETTINGS = { @@ -1049,4 +1054,298 @@ describe('ThemeController', () => { ); }); }); + + describe('setThemeConfig', () => { + beforeEach(() => { + mockGetBootstrapData.mockReturnValue( + createMockBootstrapData({ + default: {}, + dark: {}, + settings: {}, + }), + ); + + controller = new ThemeController({ + themeObject: mockThemeObject, + defaultTheme: { token: {} }, + }); + + jest.clearAllMocks(); + }); + + it('should set complete theme configuration', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + theme_settings: { + enforced: false, + allowSwitching: true, + allowOSPreference: true, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining(DEFAULT_THEME.token), + algorithm: antdThemeImport.defaultAlgorithm, + }), + ); + + expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM); + expect(controller.canSetTheme()).toBe(true); + expect(controller.canSetMode()).toBe(true); + }); + + it('should handle theme_default only', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + }; + + controller.setThemeConfig(themeConfig); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining(DEFAULT_THEME.token), + algorithm: antdThemeImport.defaultAlgorithm, + }), + ); + + expect(controller.canSetTheme()).toBe(true); + expect(controller.canSetMode()).toBe(true); + }); + + it('should handle theme_default and theme_dark without settings', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + }; + + controller.setThemeConfig(themeConfig); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining(DEFAULT_THEME.token), + }), + ); + + jest.clearAllMocks(); + controller.setThemeMode(ThemeMode.DARK); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining(DARK_THEME.token), + algorithm: antdThemeImport.darkAlgorithm, + }), + ); + }); + + it('should handle enforced theme settings', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + theme_settings: { + enforced: true, + allowSwitching: false, + allowOSPreference: false, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(controller.canSetTheme()).toBe(false); + expect(controller.canSetMode()).toBe(false); + expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT); + + expect(() => { + controller.setThemeMode(ThemeMode.DARK); + }).toThrow('User does not have permission to update the theme mode'); + }); + + it('should handle allowOSPreference: false setting', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + theme_settings: { + enforced: false, + allowSwitching: true, + allowOSPreference: false, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT); + expect(controller.canSetMode()).toBe(true); + + expect(() => { + controller.setThemeMode(ThemeMode.SYSTEM); + }).toThrow('System theme mode is not allowed'); + }); + + it('should re-determine initial mode based on new settings', () => { + mockMatchMedia.mockReturnValue({ + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }); + + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + theme_settings: { + enforced: false, + allowSwitching: false, + allowOSPreference: true, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM); + expect(controller.canSetMode()).toBe(false); + }); + + it('should apply appropriate theme after configuration', () => { + controller.setThemeMode(ThemeMode.DARK); + jest.clearAllMocks(); + + const themeConfig = { + theme_default: { + token: { + colorPrimary: '#00ff00', + }, + }, + theme_dark: { + token: { + colorPrimary: '#ff0000', + colorBgBase: '#000000', + }, + algorithm: 'dark', + }, + }; + + controller.setThemeConfig(themeConfig as SupersetThemeConfig); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining({ + colorPrimary: '#ff0000', + colorBgBase: '#000000', + }), + algorithm: antdThemeImport.darkAlgorithm, + }), + ); + }); + + it('should handle missing theme_dark gracefully', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_settings: { + allowSwitching: true, + }, + }; + + controller.setThemeConfig(themeConfig); + + jest.clearAllMocks(); + controller.setThemeMode(ThemeMode.DARK); + + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining(DEFAULT_THEME.token), + algorithm: antdThemeImport.defaultAlgorithm, + }), + ); + }); + + it('should preserve existing theme mode when possible', () => { + controller.setThemeMode(ThemeMode.DARK); + const initialMode = controller.getCurrentMode(); + + jest.clearAllMocks(); + + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + theme_settings: { + allowSwitching: true, + allowOSPreference: false, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(controller.getCurrentMode()).toBe(initialMode); + }); + + it('should trigger onChange callbacks', () => { + const changeCallback = jest.fn(); + controller.onChange(changeCallback); + + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + }; + + controller.setThemeConfig(themeConfig); + + expect(changeCallback).toHaveBeenCalledTimes(1); + expect(changeCallback).toHaveBeenCalledWith(mockThemeObject); + }); + + it('should handle partial theme_settings', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_settings: { + enforced: true, + }, + }; + + controller.setThemeConfig(themeConfig); + + expect(controller.canSetTheme()).toBe(false); + expect(controller.canSetMode()).toBe(false); + }); + + it('should handle error in theme application', () => { + mockSetConfig.mockImplementationOnce(() => { + throw new Error('Theme application error'); + }); + + const themeConfig = { + theme_default: DEFAULT_THEME, + }; + + expect(() => { + controller.setThemeConfig(themeConfig); + }).not.toThrow(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to apply theme:', + expect.any(Error), + ); + }); + + it('should update stored theme mode', () => { + const themeConfig = { + theme_default: DEFAULT_THEME, + theme_dark: DARK_THEME, + }; + + controller.setThemeConfig(themeConfig); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + 'superset-theme-mode', + expect.any(String), + ); + }); + }); }); diff --git a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx index 6d34bfb1d0..a869d70ec5 100644 --- a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx +++ b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx @@ -17,8 +17,7 @@ * under the License. */ import { ReactNode } from 'react'; -import { Theme } from '@superset-ui/core'; -import { ThemeContextType, ThemeMode } from '@superset-ui/core/theme/types'; +import { type ThemeContextType, Theme, ThemeMode } from '@superset-ui/core'; import { act, render, screen } from '@superset-ui/core/spec'; import { renderHook } from '@testing-library/react-hooks'; import { SupersetThemeProvider, useThemeContext } from '../ThemeProvider'; diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts index 055625620d..e64921900d 100644 --- a/superset-frontend/src/types/bootstrapTypes.ts +++ b/superset-frontend/src/types/bootstrapTypes.ts @@ -16,14 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { - ColorSchemeConfig, - FeatureFlagMap, - JsonObject, - LanguagePack, - Locale, - SequentialSchemeConfig, -} from '@superset-ui/core'; import { FormatLocaleDefinition } from 'd3-format'; import { TimeLocaleDefinition } from 'd3-time-format'; import { isPlainObject } from 'lodash'; @@ -31,8 +23,14 @@ import { Languages } from 'src/features/home/LanguagePicker'; import type { FlashMessage } from 'src/components'; import type { AnyThemeConfig, + ColorSchemeConfig, + FeatureFlagMap, + JsonObject, + LanguagePack, + Locale, + SequentialSchemeConfig, SerializableThemeConfig, -} from '@superset-ui/core/theme/types'; +} from '@superset-ui/core'; export type User = { createdOn?: string; @@ -189,7 +187,7 @@ export interface BootstrapThemeData { bootstrapDefaultTheme: AnyThemeConfig | null; bootstrapDarkTheme: AnyThemeConfig | null; bootstrapThemeSettings: SerializableThemeSettings | null; - hasBootstrapThemes: boolean; + hasCustomThemes: boolean; } export function isUser(user: any): user is User {
