Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Lazy load moment-timezone #29791

Merged
merged 5 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(<TimezoneSelector onTimezoneChange={onTimezoneChange} />);
expect(screen.getByLabelText('Loading')).toBeVisible();
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
});
Expand All @@ -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');
});
Expand All @@ -71,6 +79,7 @@ test('use the default timezone when an invalid timezone is provided', async () =
render(
<TimezoneSelector onTimezoneChange={onTimezoneChange} timezone="UTC" />,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('Africa/Abidjan');
});
Expand All @@ -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 () => {
Expand All @@ -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);
Expand All @@ -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(
<TimezoneSelector
Expand Down
152 changes: 89 additions & 63 deletions superset-frontend/src/components/TimezoneSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
* under the License.
*/

import { useEffect, useMemo } from 'react';
import moment from 'moment-timezone';
import { useEffect, useMemo, useState } from 'react';
import { t } from '@superset-ui/core';
import { Select } from 'src/components';
import Loading from 'src/components/Loading';

const DEFAULT_TIMEZONE = {
name: 'GMT Standard Time',
Expand All @@ -45,62 +45,6 @@ const offsetsToName = {
'060': ['GMT Standard Time - London', 'British Summer Time'],
};

const currentDate = moment();
const JANUARY = moment([2021, 1]);
const JULY = moment([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 = 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;
Expand All @@ -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 <Loading position="inline-centered" />;
}

return (
<Select
ariaLabel={t('Timezone selector')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
*/
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import {
render,
screen,
waitFor,
within,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { buildErrorTooltipMessage } from './buildErrorTooltipMessage';
import AlertReportModal, { AlertReportModalProps } from './AlertReportModal';
import { AlertObject, NotificationMethodOption } from './types';
Expand Down Expand Up @@ -519,6 +525,7 @@ test('renders default Schedule fields', async () => {
useRedux: true,
});
userEvent.click(screen.getByTestId('schedule-panel'));
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
const scheduleType = screen.getByRole('combobox', {
name: /schedule type/i,
});
Expand Down
Loading
Loading