Skip to content

Commit

Permalink
perf: Lazy load moment-timezone (apache#29791)
Browse files Browse the repository at this point in the history
  • Loading branch information
kgabryje authored and WanjohiWanjohi committed Aug 6, 2024
1 parent 0e51ea5 commit f3944a3
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 71 deletions.
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

0 comments on commit f3944a3

Please sign in to comment.