| /** |
| * 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 { 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', |
| value: 'Africa/Abidjan', // timezones are deduped by the first alphabetical value |
| }; |
| |
| const MIN_SELECT_WIDTH = '400px'; |
| |
| const offsetsToName = { |
| '-300-240': ['Eastern Standard Time', 'Eastern Daylight Time'], |
| '-360-300': ['Central Standard Time', 'Central Daylight Time'], |
| '-420-360': ['Mountain Standard Time', 'Mountain Daylight Time'], |
| '-420-420': [ |
| 'Mountain Standard Time - Phoenix', |
| 'Mountain Standard Time - Phoenix', |
| ], |
| '-480-420': ['Pacific Standard Time', 'Pacific Daylight Time'], |
| '-540-480': ['Alaska Standard Time', 'Alaska Daylight Time'], |
| '-600-600': ['Hawaii Standard Time', 'Hawaii Daylight Time'], |
| '60120': ['Central European Time', 'Central European Daylight Time'], |
| '00': [DEFAULT_TIMEZONE.name, DEFAULT_TIMEZONE.name], |
| '060': ['GMT Standard Time - London', 'British Summer Time'], |
| }; |
| |
| export type TimezoneSelectorProps = { |
| onTimezoneChange: (value: string) => void; |
| timezone?: string | null; |
| minWidth?: string; |
| }; |
| |
| export default function TimezoneSelector({ |
| onTimezoneChange, |
| timezone, |
| minWidth = MIN_SELECT_WIDTH, // smallest size for current values |
| }: TimezoneSelectorProps) { |
| 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 (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')} |
| css={{ minWidth }} |
| onChange={tz => onTimezoneChange(tz as string)} |
| value={validTimezone} |
| options={TIMEZONE_OPTIONS} |
| sortComparator={TIMEZONE_OPTIONS_SORT_COMPARATOR} |
| /> |
| ); |
| } |