mirror of
https://github.com/supabase/supabase.git
synced 2026-05-09 10:19:50 -04:00
d8bb0ade65
## Problem The dashboard renders all timestamps in the browser's local timezone. When debugging app issues, users often want to see logs and timestamps in a different timezone (e.g. their app's deployment region) without changing their OS clock. ## Fix - New Timezone submenu in the user-avatar dropdown, sitting next to the existing Theme picker. Search-as-you-type combobox over the full IANA catalog plus an Auto detect option. - Selection persists in localStorage (`supabase-ui-timezone`) and survives `clearLocalStorage()`. No backend schema change. - New `lib/datetime.tsx` exposes pure timezone-aware formatters (`formatDateTime`, `formatDate`, `formatTime`, `formatFromNow`, `toTimezone`) plus a `TimezoneProvider` and matching React hooks (`useTimezone`, `useFormatDateTime`, ...). The pure functions take `tz` explicitly so they're easy to unit test (17 vitest cases covering DST transitions, multi-tz formatting, unix-micro/Date inputs, invalid-tz fallback). - The selected timezone propagates to every existing `<TimestampInfo>` in Studio via a new `TimestampInfoProvider` context exported from `ui-patterns`. No per-callsite changes needed for those ~20+ surfaces. - The `UnifiedLogs` date column migrates off `date-fns` to the new `useFormatDateTime` hook (the rest of the date-fns callers stay as-is, since they're either internal range math or non-display). - `ALL_TIMEZONES` (~600 entries) moves out of `PITR.constants.ts` into a shared `lib/constants/timezones.ts`. PITR keeps a re-export shim so its callers don't move. New `TIMEZONES_BY_IANA` dedupes the catalog by primary IANA name (the original list contains both PDT and PST rows for `America/Los_Angeles`, etc.) and `findTimezoneByIana` provides reverse lookup. - Telemetry: `timezone_picker_clicked` PostHog event with `previousTimezone`, `nextTimezone`, `isAutoDetected` properties. Notes for reviewers: - Bare `dayjs(x).format(...)` calls (~157 files) intentionally still render in browser-local time. Surfaces opt in by switching to the new wrappers, so this PR is the abstraction plus logs adoption; broader migration is a follow-up. - Two `// prettier-ignore` lines (`apps/studio/pages/_app.tsx`, `apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx`) work around a pre-existing local-tooling issue where `prettier-plugin-sql-cst` strips angle-bracket type arguments under certain conditions. Project's pinned prettier (3.8.1) does not strip; the issue surfaces with a globally-installed prettier. Worth tracking separately. - Hydration: `guessLocalTimezone()` and `useLocalStorageQuery` are client-only. Studio is mostly CSR via the Pages Router, but any SSR'd `<TimestampInfo>` may briefly render in the server's tz before client hydration. Existing behavior already had this mismatch with `.local()`; this PR does not regress it. - Backend timestamps round-tripped through query params and mutations stay UTC. The picker is display-only. ## How to test - Run `pnpm dev:studio`, sign in. - Open the user avatar dropdown (top right). Hover Timezone. - Search for "tokyo", pick `(UTC+09:00) Osaka, Sapporo, Tokyo`. - Open any project, navigate to Logs (e.g. `Project > Logs > Edge Functions`). Hover a log row's timestamp; the popover should show UTC, the chosen tz (`Asia/Tokyo`), and the relative time. Visible cell text should be in JST. - Visit any page that uses `<TimestampInfo>` (Database > Backups, Project Pause state, Edge Function details). Same tooltip should reflect Asia/Tokyo. - Refresh the page; timezone is still Asia/Tokyo. - Reopen the picker, choose Auto detect; timestamps revert to browser local. - Run `pnpm --filter studio test lib/datetime.test.ts`. 17 tests should pass. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Timezone selector added to the user menu with auto-detect and manual override * App-wide timezone provider and hooks plus a shared timezone catalog for consistent timezone-aware display * Timestamp components accept an optional timezone prop and respect user preference (persisted) * **Bug Fixes / Improvements** * Logs and timestamp displays now use the new timezone formatting hooks * **Tests** * Added comprehensive datetime and timezone catalog tests * **Telemetry** * Telemetry event added for timezone picker interactions <!-- end of auto-generated comment: release notes by coderabbit.ai -->
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { CheckIcon } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import {
|
|
cn,
|
|
Command_Shadcn_,
|
|
CommandEmpty_Shadcn_,
|
|
CommandGroup_Shadcn_,
|
|
CommandInput_Shadcn_,
|
|
CommandItem_Shadcn_,
|
|
CommandList_Shadcn_,
|
|
DropdownMenuPortal,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
ScrollArea,
|
|
} from 'ui'
|
|
|
|
import { findTimezoneByIana, 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 { timezone, storedTimezone, setTimezone, isAutoDetected } = useTimezone()
|
|
const track = useTrack()
|
|
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 findTimezoneByIana(timezone)?.text ?? 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_Shadcn_>
|
|
<CommandInput_Shadcn_ placeholder="Search timezone..." className="h-9" />
|
|
<CommandList_Shadcn_>
|
|
<CommandEmpty_Shadcn_>No timezones found</CommandEmpty_Shadcn_>
|
|
<CommandGroup_Shadcn_>
|
|
<ScrollArea className="h-72">
|
|
<CommandItem_Shadcn_
|
|
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_Shadcn_>
|
|
{TIMEZONES_BY_IANA.map((entry) => {
|
|
const ianaName = entry.utc[0]
|
|
const isSelected = !isAutoDetected && storedTimezone === ianaName
|
|
return (
|
|
<CommandItem_Shadcn_
|
|
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={`${entry.text} ${ianaName}`}
|
|
onSelect={() => handleSelect(ianaName)}
|
|
>
|
|
{entry.text}
|
|
<CheckIcon
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
isSelected ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
</CommandItem_Shadcn_>
|
|
)
|
|
})}
|
|
</ScrollArea>
|
|
</CommandGroup_Shadcn_>
|
|
</CommandList_Shadcn_>
|
|
</Command_Shadcn_>
|
|
</DropdownMenuSubContent>
|
|
</DropdownMenuPortal>
|
|
</DropdownMenuSub>
|
|
)
|
|
}
|