diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 1a4a8b71f..dbebf1311 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -261,6 +261,7 @@
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.9.0",
"mock-socket": "^9.3.1",
+ "moment-locales-webpack-plugin": "^1.2.0",
"node-fetch": "^2.6.7",
"po2json": "^0.4.5",
"prettier": "3.1.0",
@@ -41429,6 +41430,13 @@
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
},
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.escape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
@@ -47666,6 +47674,20 @@
"node": "*"
}
},
+ "node_modules/moment-locales-webpack-plugin": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
+ "integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash.difference": "^4.5.0"
+ },
+ "peerDependencies": {
+ "moment": "^2.8.0",
+ "webpack": "^1 || ^2 || ^3 || ^4 || ^5"
+ }
+ },
"node_modules/moment-timezone": {
"version": "0.5.44",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.44.tgz",
@@ -99486,6 +99508,12 @@
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
},
+ "lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true
+ },
"lodash.escape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
@@ -103398,6 +103426,15 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
},
+ "moment-locales-webpack-plugin": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
+ "integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
+ "dev": true,
+ "requires": {
+ "lodash.difference": "^4.5.0"
+ }
+ },
"moment-timezone": {
"version": "0.5.44",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.44.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 1283bd6a8..aae88e280 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -326,6 +326,7 @@
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.9.0",
"mock-socket": "^9.3.1",
+ "moment-locales-webpack-plugin": "^1.2.0",
"node-fetch": "^2.6.7",
"po2json": "^0.4.5",
"prettier": "3.1.0",
diff --git a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx
index 81dbc6fdc..f30eeb2d5 100644
--- a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx
+++ b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx
@@ -18,7 +18,12 @@
*/
import { FC } from 'react';
-import { render, waitFor, screen } from 'spec/helpers/testing-library';
+import {
+ render,
+ waitFor,
+ screen,
+ waitForElementToBeRemoved,
+} from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import type { TimezoneSelectorProps } from './index';
@@ -44,6 +49,7 @@ test('render timezones in correct order for daylight saving time', async () => {
/>,
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
const searchInput = screen.getByRole('combobox');
userEvent.click(searchInput);
diff --git a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx
index 173118c7e..c10ecf2b0 100644
--- a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx
+++ b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx
@@ -16,10 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-import moment from 'moment-timezone';
import { FC } from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import moment from 'moment-timezone';
import userEvent from '@testing-library/user-event';
+import {
+ render,
+ screen,
+ waitFor,
+ waitForElementToBeRemoved,
+} from 'spec/helpers/testing-library';
import type { TimezoneSelectorProps } from './index';
const loadComponent = (mockCurrentTime?: string) => {
@@ -48,6 +53,8 @@ test('use the timezone from `moment` if no timezone provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render();
+ expect(screen.getByLabelText('Loading')).toBeVisible();
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
});
@@ -61,6 +68,7 @@ test('update to closest deduped timezone when timezone is provided', async () =>
timezone="America/Los_Angeles"
/>,
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Vancouver');
});
@@ -71,6 +79,7 @@ test('use the default timezone when an invalid timezone is provided', async () =
render(
,
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('Africa/Abidjan');
});
@@ -84,12 +93,13 @@ test('render timezones in correct oder for standard time', async () => {
timezone="America/Nassau"
/>,
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
openSelectMenu();
const options = await getSelectOptions();
- expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)');
+ expect(options[0]).toHaveTextContent('GMT -04:00 (Eastern Daylight Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
- expect(options[3]).toHaveTextContent('GMT -10:00 (America/Adak)');
+ expect(options[3]).toHaveTextContent('GMT -09:30 (Pacific/Marquesas)');
});
test('can select a timezone values and returns canonical timezone name', async () => {
@@ -101,13 +111,13 @@ test('can select a timezone values and returns canonical timezone name', async (
timezone="Africa/Abidjan"
/>,
);
-
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
openSelectMenu();
const searchInput = screen.getByRole('combobox');
// search for mountain time
await userEvent.type(searchInput, 'mou', { delay: 10 });
- const findTitle = 'GMT -07:00 (Mountain Standard Time)';
+ const findTitle = 'GMT -06:00 (Mountain Daylight Time)';
const selectOption = await screen.findByTitle(findTitle);
userEvent.click(selectOption);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
@@ -123,6 +133,7 @@ test('can update props and rerender with different values', async () => {
timezone="Asia/Dubai"
/>,
);
+ await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(screen.getByTitle('GMT +04:00 (Asia/Dubai)')).toBeInTheDocument();
rerender(
- JANUARY.tz(name).utcOffset().toString() +
- JULY.tz(name).utcOffset().toString();
-
-const getTimezoneName = (name: string) => {
- const offsets = getOffsetKey(name);
- return (
- (currentDate.tz(name).isDST()
- ? offsetsToName[offsets]?.[1]
- : offsetsToName[offsets]?.[0]) || name
- );
-};
-
-const ALL_ZONES = moment.tz
- .countries()
- .map(country => moment.tz.zonesForCountry(country, true))
- .flat();
-
-const TIMEZONES: moment.MomentZoneOffset[] = [];
-ALL_ZONES.forEach(zone => {
- if (
- !TIMEZONES.find(
- option => getOffsetKey(option.name) === getOffsetKey(zone.name),
- )
- ) {
- TIMEZONES.push(zone); // dedupe zones by offsets
- }
-});
-
-const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
- label: `GMT ${moment
- .tz(currentDate, zone.name)
- .format('Z')} (${getTimezoneName(zone.name)})`,
- value: zone.name,
- offsets: getOffsetKey(zone.name),
- timezoneName: zone.name,
-}));
-
-const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
- a: (typeof TIMEZONE_OPTIONS)[number],
- b: (typeof TIMEZONE_OPTIONS)[number],
-) =>
- moment.tz(currentDate, a.timezoneName).utcOffset() -
- moment.tz(currentDate, b.timezoneName).utcOffset();
-
-// pre-sort timezone options by time offset
-TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
-
-const matchTimezoneToOptions = (timezone: string) =>
- TIMEZONE_OPTIONS.find(option => option.offsets === getOffsetKey(timezone))
- ?.value || DEFAULT_TIMEZONE.value;
-
export type TimezoneSelectorProps = {
onTimezoneChange: (value: string) => void;
timezone?: string | null;
@@ -112,18 +56,100 @@ export default function TimezoneSelector({
timezone,
minWidth = MIN_SELECT_WIDTH, // smallest size for current values
}: TimezoneSelectorProps) {
- const validTimezone = useMemo(
- () => matchTimezoneToOptions(timezone || moment.tz.guess()),
- [timezone],
- );
+ const [momentLib, setMomentLib] = useState<
+ typeof import('moment-timezone') | null
+ >(null);
+
+ useEffect(() => {
+ import('moment-timezone').then(momentLib =>
+ setMomentLib(() => momentLib.default),
+ );
+ }, []);
+
+ const { TIMEZONE_OPTIONS, TIMEZONE_OPTIONS_SORT_COMPARATOR, validTimezone } =
+ useMemo(() => {
+ if (!momentLib) {
+ return {};
+ }
+ const currentDate = momentLib();
+ const JANUARY = momentLib([2021, 1]);
+ const JULY = momentLib([2021, 7]);
+
+ const getOffsetKey = (name: string) =>
+ JANUARY.tz(name).utcOffset().toString() +
+ JULY.tz(name).utcOffset().toString();
+
+ const getTimezoneName = (name: string) => {
+ const offsets = getOffsetKey(name);
+ return (
+ (currentDate.tz(name).isDST()
+ ? offsetsToName[offsets]?.[1]
+ : offsetsToName[offsets]?.[0]) || name
+ );
+ };
+
+ const ALL_ZONES = momentLib.tz
+ .countries()
+ .map(country => momentLib.tz.zonesForCountry(country, true))
+ .flat();
+
+ const TIMEZONES: import('moment-timezone').MomentZoneOffset[] = [];
+ ALL_ZONES.forEach(zone => {
+ if (
+ !TIMEZONES.find(
+ option => getOffsetKey(option.name) === getOffsetKey(zone.name),
+ )
+ ) {
+ TIMEZONES.push(zone); // dedupe zones by offsets
+ }
+ });
+
+ const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
+ label: `GMT ${momentLib
+ .tz(currentDate, zone.name)
+ .format('Z')} (${getTimezoneName(zone.name)})`,
+ value: zone.name,
+ offsets: getOffsetKey(zone.name),
+ timezoneName: zone.name,
+ }));
+
+ const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
+ a: (typeof TIMEZONE_OPTIONS)[number],
+ b: (typeof TIMEZONE_OPTIONS)[number],
+ ) =>
+ momentLib.tz(currentDate, a.timezoneName).utcOffset() -
+ momentLib.tz(currentDate, b.timezoneName).utcOffset();
+
+ // pre-sort timezone options by time offset
+ TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
+
+ const matchTimezoneToOptions = (timezone: string) =>
+ TIMEZONE_OPTIONS.find(
+ option => option.offsets === getOffsetKey(timezone),
+ )?.value || DEFAULT_TIMEZONE.value;
+
+ const validTimezone = matchTimezoneToOptions(
+ timezone || momentLib.tz.guess(),
+ );
+
+ return {
+ TIMEZONE_OPTIONS,
+ TIMEZONE_OPTIONS_SORT_COMPARATOR,
+ validTimezone,
+ };
+ }, [momentLib, timezone]);
// force trigger a timezone update if provided `timezone` is not invalid
useEffect(() => {
- if (timezone !== validTimezone) {
+ if (validTimezone && timezone !== validTimezone) {
onTimezoneChange(validTimezone);
}
}, [validTimezone, onTimezoneChange, timezone]);
+ if (!TIMEZONE_OPTIONS || !TIMEZONE_OPTIONS_SORT_COMPARATOR) {
+ return ;
+ }
+
return (