diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx index 5e243224f1a..a2da3a5544c 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx @@ -42,12 +42,15 @@ jest.mock('@superset-ui/core', () => ({ })); beforeEach(() => { + jest.useRealTimers(); + fetchMock.removeRoutes(); fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT }); setupDatasourceEditorMocks(); jest.clearAllMocks(); }); afterEach(async () => { + jest.useRealTimers(); await cleanupAsyncOperations(); fetchMock.clearHistory().removeRoutes(); // Reset module mock since jest.fn() doesn't support mockRestore() diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index 961a558bb96..58dba03d08b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -506,7 +506,7 @@ test('deletes a filter including dependencies', async () => { const filterTabs = within(filterContainer).getAllByRole('tab'); const deleteIcon = filterTabs[1].querySelector('[data-icon="delete"]'); fireEvent.click(deleteIcon!); - userEvent.click(screen.getByRole('button', { name: SAVE_REGEX })); + await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX })); await waitFor(() => expect(onSave).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx index 6c4169413de..d69594fbdc0 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx @@ -35,6 +35,7 @@ import { jest.setTimeout(30000); beforeEach(() => { + jest.useRealTimers(); setupMocks(); jest.clearAllMocks(); }); diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index 4595e822ebd..97c3f5c2ade 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -17,7 +17,8 @@ * under the License. */ import { configure, LanguagePack } from '@apache-superset/core/ui'; -import { makeApi, initFeatureFlags, SupersetClient } from '@superset-ui/core'; +import { logging } from '@apache-superset/core'; +import { makeApi, initFeatureFlags } from '@superset-ui/core'; import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; import setupClient from './setup/setupClient'; import setupColors from './setup/setupColors'; @@ -25,6 +26,7 @@ import setupFormatters from './setup/setupFormatters'; import setupDashboardComponents from './setup/setupDashboardComponents'; import { User } from './types/bootstrapTypes'; import getBootstrapData, { applicationRoot } from './utils/getBootstrapData'; +import { makeUrl } from './utils/pathUtils'; import './hooks/useLocale'; // Import dayjs plugin types for global TypeScript support @@ -37,62 +39,97 @@ import 'dayjs/plugin/duration'; import 'dayjs/plugin/updateLocale'; import 'dayjs/plugin/localizedFormat'; -configure(); +let initPromise: Promise | null = null; -// Grab initial bootstrap data -const bootstrapData = getBootstrapData(); +const LANGUAGE_PACK_REQUEST_TIMEOUT_MS = 5000; -setupFormatters( - bootstrapData.common.d3_format, - bootstrapData.common.d3_time_format, -); - -// Setup SupersetClient early so we can fetch language pack -setupClient({ appRoot: applicationRoot() }); - -// Load language pack before anything else -(async () => { - const lang = bootstrapData.common.locale || 'en'; - if (lang !== 'en') { - try { - // Second call to configure to set the language pack - const { json } = await SupersetClient.get({ - endpoint: `/superset/language_pack/${lang}/`, - }); - configure({ languagePack: json as LanguagePack }); - dayjs.locale(lang); - } catch (err) { - console.warn( - 'Failed to fetch language pack, falling back to default.', - err, - ); - configure(); - dayjs.locale('en'); - } +export default function initPreamble(): Promise { + if (initPromise) { + return initPromise; } - // Continue with rest of setup - initFeatureFlags(bootstrapData.common.feature_flags); + initPromise = (async () => { + configure(); - setupColors( - bootstrapData.common.extra_categorical_color_schemes, - bootstrapData.common.extra_sequential_color_schemes, - ); + // Grab initial bootstrap data + const bootstrapData = getBootstrapData(); - setupDashboardComponents(); + setupFormatters( + bootstrapData.common.d3_format, + bootstrapData.common.d3_time_format, + ); - const getMe = makeApi({ - method: 'GET', - endpoint: '/api/v1/me/', + // Setup SupersetClient early so we can fetch language pack + setupClient({ appRoot: applicationRoot() }); + + // Load language pack before rendering + // Use native fetch to avoid race condition with SupersetClient initialization + const lang = bootstrapData.common.locale || 'en'; + if (lang !== 'en') { + const abortController = new AbortController(); + const timeoutId = window.setTimeout(() => { + abortController.abort(); + }, LANGUAGE_PACK_REQUEST_TIMEOUT_MS); + + try { + const languagePackUrl = makeUrl(`/superset/language_pack/${lang}/`); + const resp = await fetch(languagePackUrl, { + signal: abortController.signal, + }); + if (!resp.ok) { + throw new Error(`Failed to fetch language pack: ${resp.status}`); + } + const json = await resp.json(); + configure({ languagePack: json as LanguagePack }); + dayjs.locale(lang); + } catch (err) { + logging.warn( + 'Failed to fetch language pack, falling back to default.', + err, + ); + configure(); + dayjs.locale('en'); + } finally { + window.clearTimeout(timeoutId); + } + } + + // Continue with rest of setup + initFeatureFlags(bootstrapData.common.feature_flags); + + setupColors( + bootstrapData.common.extra_categorical_color_schemes, + bootstrapData.common.extra_sequential_color_schemes, + ); + + setupDashboardComponents(); + + const getMe = makeApi({ + method: 'GET', + endpoint: '/api/v1/me/', + }); + + if (bootstrapData.user?.isActive) { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + getMe().catch(() => { + // SupersetClient will redirect to login on 401 + }); + } + }); + } + })().catch(err => { + // Allow retry by clearing the cached promise on failure + initPromise = null; + throw err; }); - if (bootstrapData.user?.isActive) { - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - getMe().catch(() => { - // SupersetClient will redirect to login on 401 - }); - } - }); - } -})(); + return initPromise; +} + +// This module is prepended to multiple webpack entrypoints (see `webpack.config.js`). +// Kick off initialization eagerly, while still allowing entrypoints to `await` it +// before rendering when needed (e.g. the login page). +initPreamble().catch(err => { + logging.warn('Preamble initialization failed.', err); +}); diff --git a/superset-frontend/src/views/index.tsx b/superset-frontend/src/views/index.tsx index 79784fbc9ba..c96528c9f79 100644 --- a/superset-frontend/src/views/index.tsx +++ b/superset-frontend/src/views/index.tsx @@ -19,6 +19,20 @@ import 'src/public-path'; import ReactDOM from 'react-dom'; -import App from './App'; +import { logging } from '@apache-superset/core'; +import initPreamble from 'src/preamble'; -ReactDOM.render(, document.getElementById('app')); +const appMountPoint = document.getElementById('app'); + +if (appMountPoint) { + (async () => { + try { + await initPreamble(); + } finally { + const { default: App } = await import(/* webpackMode: "eager" */ './App'); + ReactDOM.render(, appMountPoint); + } + })().catch(err => { + logging.error('Unhandled error during app initialization', err); + }); +} diff --git a/superset/views/base.py b/superset/views/base.py index afca4d2c448..15b35d94374 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -476,7 +476,12 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument and bool(available_specs[GSheetsEngineSpec]) ) - language = locale.language if locale else "en" + if isinstance(locale, Locale): + language = locale.language + elif isinstance(locale, str): + language = locale + else: + language = app.config.get("BABEL_DEFAULT_LOCALE", "en") auth_type = app.config["AUTH_TYPE"] auth_user_registration = app.config["AUTH_USER_REGISTRATION"] frontend_config["AUTH_USER_REGISTRATION"] = auth_user_registration