mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
## Problem Restore timezone labels use fixed UTC offsets from the timezone catalog. Madrid displays UTC+01:00 during daylight saving time even though the reported restore timestamp correctly uses UTC+02:00. ## Fix Calculate display offsets from the restore instant and the timezone's IANA identifier using Intl. Apply the same formatter to dropdown options, the selected timezone, and PITR confirmation while preserving existing restore calculations and timezone selection identity. Fixes https://linear.app/supabase/issue/FE-4612 ## How to test - Choose Madrid in restore-to-new-project or PITR and compare winter and summer restore dates. Expected result: UTC+01:00 in winter and UTC+02:00 during daylight saving time, including the confirmation label. - Check UTC, New York, Kolkata, and Kathmandu. Expected result: correct zero, negative, and fractional offsets. - Focused Vitest suite: 15 tests passed, including both Madrid DST boundaries and timestamp preservation. The component test verifies visible trigger and option labels update after a date change, Madrid search results, selection preserving the original timezone object, and popover closure. It also passes under UTC and Hong Kong browser timezones. Prettier and diff checks passed. - All CI checks passed for the latest commit, including Studio tests, all four Next.js/TanStack E2E shards, typecheck, lint ratchet, formatting, spelling, unused-code checks, and Docker build. Full local typecheck and ESLint validation were blocked by the shared dependency installation being out of sync with this checkout. Calculation audit confirmed the reported October 9 Madrid 05:12 to UTC 03:12 conversion. Separate existing date-control issues were found when browser and restore timezones differ and around the repeated autumn hour; those are outside this label fix and relevant to FE-4129. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import { CheckIcon } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import {
|
|
cn,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
DropdownMenuPortal,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
ScrollArea,
|
|
} from 'ui'
|
|
|
|
import {
|
|
getTimezoneLabel,
|
|
getTimezoneLabelByIana,
|
|
TIMEZONES_BY_IANA,
|
|
} from '@/lib/constants/timezones'
|
|
import { useTimezone } from '@/lib/datetime'
|
|
import { guessLocalTimezone } from '@/lib/dayjs'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
const AUTO_OPTION_VALUE = '__auto__'
|
|
|
|
export const TimezoneDropdown = () => {
|
|
const track = useTrack()
|
|
const { timezone, storedTimezone, setTimezone, isAutoDetected } = useTimezone()
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
// The "Auto detect" row always advertises the browser's own timezone, even
|
|
// when the user is currently overriding it with a manual pick.
|
|
const browserTimezone = useMemo(() => guessLocalTimezone(), [])
|
|
|
|
const triggerLabel = useMemo(() => {
|
|
return getTimezoneLabelByIana(timezone)
|
|
}, [timezone])
|
|
|
|
const handleSelect = (nextStored: string) => {
|
|
setTimezone(nextStored)
|
|
const resolvedNext = nextStored || guessLocalTimezone()
|
|
track('timezone_picker_clicked', {
|
|
previousTimezone: timezone,
|
|
nextTimezone: resolvedNext,
|
|
isAutoDetected: nextStored === '',
|
|
source: 'user_dropdown',
|
|
})
|
|
setOpen(false)
|
|
}
|
|
|
|
return (
|
|
<DropdownMenuSub open={open} onOpenChange={setOpen}>
|
|
<DropdownMenuSubTrigger className="flex gap-2 cursor-pointer">
|
|
<div className="flex flex-col min-w-0">
|
|
<span>Timezone</span>
|
|
<span className="text-xs text-foreground-lighter truncate" title={triggerLabel}>
|
|
{isAutoDetected ? `Auto (${timezone})` : triggerLabel}
|
|
</span>
|
|
</div>
|
|
</DropdownMenuSubTrigger>
|
|
<DropdownMenuPortal>
|
|
<DropdownMenuSubContent className="p-0 w-[320px]" sideOffset={4}>
|
|
<Command>
|
|
<CommandInput placeholder="Search timezone..." className="h-9" />
|
|
<CommandList>
|
|
<CommandEmpty>No timezones found</CommandEmpty>
|
|
<CommandGroup>
|
|
<ScrollArea className="h-72">
|
|
<CommandItem
|
|
key={AUTO_OPTION_VALUE}
|
|
value={`Auto detect ${browserTimezone}`}
|
|
onSelect={() => handleSelect('')}
|
|
>
|
|
<div className="flex flex-col">
|
|
<span>Auto detect</span>
|
|
<span className="text-xs text-foreground-lighter">{browserTimezone}</span>
|
|
</div>
|
|
<CheckIcon
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
isAutoDetected ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
<CommandItem
|
|
key="UTC"
|
|
value="UTC Coordinated Universal Time"
|
|
onSelect={() => handleSelect('UTC')}
|
|
>
|
|
{'(UTC+00:00) Coordinated Universal Time'}
|
|
<CheckIcon
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
!isAutoDetected && storedTimezone === 'UTC' ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
{TIMEZONES_BY_IANA.map((entry) => {
|
|
const ianaName = entry.utc[0]
|
|
const isSelected = !isAutoDetected && storedTimezone === ianaName
|
|
return (
|
|
<CommandItem
|
|
key={ianaName}
|
|
// CommandItem matches against the `value` prop for the input filter — include
|
|
// both the human label and the IANA name so search works for either.
|
|
value={`${getTimezoneLabel(entry)} ${ianaName}`}
|
|
onSelect={() => handleSelect(ianaName)}
|
|
>
|
|
{getTimezoneLabel(entry)}
|
|
<CheckIcon
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
isSelected ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</ScrollArea>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</DropdownMenuSubContent>
|
|
</DropdownMenuPortal>
|
|
</DropdownMenuSub>
|
|
)
|
|
}
|