airbnb/caravel

View on GitHub
superset-frontend/src/components/TimezoneSelector/index.tsx

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * 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}
    />
  );
}