diff --git a/apps/studio/components/interfaces/Account/Preferences/TimezoneSettings.tsx b/apps/studio/components/interfaces/Account/Preferences/TimezoneSettings.tsx
new file mode 100644
index 00000000000..24ebb78d7ea
--- /dev/null
+++ b/apps/studio/components/interfaces/Account/Preferences/TimezoneSettings.tsx
@@ -0,0 +1,166 @@
+import { useFlag } from 'common'
+import { CheckIcon, ChevronsUpDown, Globe } from 'lucide-react'
+import { useId, useMemo, useState } from 'react'
+import {
+ Button,
+ Card,
+ CardContent,
+ cn,
+ Command_Shadcn_,
+ CommandEmpty_Shadcn_,
+ CommandGroup_Shadcn_,
+ CommandInput_Shadcn_,
+ CommandItem_Shadcn_,
+ CommandList_Shadcn_,
+ Popover_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ ScrollArea,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionDescription,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+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 TimezoneSettings = () => {
+ const timezonePickerEnabled = useFlag('timezonePicker')
+ const { timezone, storedTimezone, setTimezone, isAutoDetected } = useTimezone()
+ const track = useTrack()
+ const [open, setOpen] = useState(false)
+ const listboxId = useId()
+
+ // Browser timezone is captured once and stays stable even when the user has
+ // overridden the dashboard timezone — that's the value the "Auto detect"
+ // option will revert to.
+ const browserTimezone = useMemo(() => guessLocalTimezone(), [])
+
+ const triggerLabel = useMemo(() => findTimezoneByIana(timezone)?.text ?? timezone, [timezone])
+
+ if (!timezonePickerEnabled) return null
+
+ const handleSelect = (nextStored: string) => {
+ setTimezone(nextStored)
+ const resolvedNext = nextStored || guessLocalTimezone()
+ track('timezone_picker_clicked', {
+ previousTimezone: timezone,
+ nextTimezone: resolvedNext,
+ isAutoDetected: nextStored === '',
+ source: 'account_preferences',
+ })
+ setOpen(false)
+ }
+
+ return (
+
+
+
+ Timezone
+
+ Choose how dates and times in logs and other dashboard surfaces are displayed.
+
+
+
+
+
+
+
+
+
+ }
+ iconRight={ }
+ >
+
+ {isAutoDetected ? `Auto detect (${timezone})` : triggerLabel}
+
+
+
+
+
+
+
+ No timezones found
+
+
+ handleSelect('')}
+ >
+
+ Auto detect
+
+ {browserTimezone}
+
+
+
+
+ {TIMEZONES_BY_IANA.map((entry) => {
+ const ianaName = entry.utc[0]
+ const isSelected = !isAutoDetected && storedTimezone === ianaName
+ return (
+ handleSelect(ianaName)}
+ >
+ {entry.text}
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts b/apps/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts
index 354e142106d..08ee0bb3b29 100644
--- a/apps/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts
+++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts
@@ -1,1188 +1,3 @@
-// https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json
-export const ALL_TIMEZONES = [
- {
- value: 'Dateline Standard Time',
- abbr: 'DST',
- offset: -12,
- isdst: false,
- text: '(UTC-12:00) International Date Line West',
- utc: ['Etc/GMT+12'],
- },
- {
- value: 'UTC-11',
- abbr: 'U',
- offset: -11,
- isdst: false,
- text: '(UTC-11:00) Coordinated Universal Time-11',
- utc: ['Etc/GMT+11', 'Pacific/Midway', 'Pacific/Niue', 'Pacific/Pago_Pago'],
- },
- {
- value: 'Hawaiian Standard Time',
- abbr: 'HST',
- offset: -10,
- isdst: false,
- text: '(UTC-10:00) Hawaii',
- utc: [
- 'Etc/GMT+10',
- 'Pacific/Honolulu',
- 'Pacific/Johnston',
- 'Pacific/Rarotonga',
- 'Pacific/Tahiti',
- ],
- },
- {
- value: 'Alaskan Standard Time',
- abbr: 'AKDT',
- offset: -8,
- isdst: true,
- text: '(UTC-09:00) Alaska',
- utc: [
- 'America/Anchorage',
- 'America/Juneau',
- 'America/Nome',
- 'America/Sitka',
- 'America/Yakutat',
- ],
- },
- {
- value: 'Pacific Standard Time (Mexico)',
- abbr: 'PDT',
- offset: -7,
- isdst: true,
- text: '(UTC-08:00) Baja California',
- utc: ['America/Santa_Isabel'],
- },
- {
- value: 'Pacific Daylight Time',
- abbr: 'PDT',
- offset: -7,
- isdst: true,
- text: '(UTC-07:00) Pacific Daylight Time (US & Canada)',
- utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver'],
- },
- {
- value: 'Pacific Standard Time',
- abbr: 'PST',
- offset: -8,
- isdst: false,
- text: '(UTC-08:00) Pacific Standard Time (US & Canada)',
- utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver', 'PST8PDT'],
- },
- {
- value: 'US Mountain Standard Time',
- abbr: 'UMST',
- offset: -7,
- isdst: false,
- text: '(UTC-07:00) Arizona',
- utc: [
- 'America/Creston',
- 'America/Dawson',
- 'America/Dawson_Creek',
- 'America/Hermosillo',
- 'America/Phoenix',
- 'America/Whitehorse',
- 'Etc/GMT+7',
- ],
- },
- {
- value: 'Mountain Standard Time (Mexico)',
- abbr: 'MDT',
- offset: -6,
- isdst: true,
- text: '(UTC-07:00) Chihuahua, La Paz, Mazatlan',
- utc: ['America/Chihuahua', 'America/Mazatlan'],
- },
- {
- value: 'Mountain Standard Time',
- abbr: 'MDT',
- offset: -6,
- isdst: true,
- text: '(UTC-07:00) Mountain Time (US & Canada)',
- utc: [
- 'America/Boise',
- 'America/Cambridge_Bay',
- 'America/Denver',
- 'America/Edmonton',
- 'America/Inuvik',
- 'America/Ojinaga',
- 'America/Yellowknife',
- 'MST7MDT',
- ],
- },
- {
- value: 'Central America Standard Time',
- abbr: 'CAST',
- offset: -6,
- isdst: false,
- text: '(UTC-06:00) Central America',
- utc: [
- 'America/Belize',
- 'America/Costa_Rica',
- 'America/El_Salvador',
- 'America/Guatemala',
- 'America/Managua',
- 'America/Tegucigalpa',
- 'Etc/GMT+6',
- 'Pacific/Galapagos',
- ],
- },
- {
- value: 'Central Standard Time',
- abbr: 'CDT',
- offset: -5,
- isdst: true,
- text: '(UTC-06:00) Central Time (US & Canada)',
- utc: [
- 'America/Chicago',
- 'America/Indiana/Knox',
- 'America/Indiana/Tell_City',
- 'America/Matamoros',
- 'America/Menominee',
- 'America/North_Dakota/Beulah',
- 'America/North_Dakota/Center',
- 'America/North_Dakota/New_Salem',
- 'America/Rainy_River',
- 'America/Rankin_Inlet',
- 'America/Resolute',
- 'America/Winnipeg',
- 'CST6CDT',
- ],
- },
- {
- value: 'Central Standard Time (Mexico)',
- abbr: 'CDT',
- offset: -5,
- isdst: true,
- text: '(UTC-06:00) Guadalajara, Mexico City, Monterrey',
- utc: [
- 'America/Bahia_Banderas',
- 'America/Cancun',
- 'America/Merida',
- 'America/Mexico_City',
- 'America/Monterrey',
- ],
- },
- {
- value: 'Canada Central Standard Time',
- abbr: 'CCST',
- offset: -6,
- isdst: false,
- text: '(UTC-06:00) Saskatchewan',
- utc: ['America/Regina', 'America/Swift_Current'],
- },
- {
- value: 'SA Pacific Standard Time',
- abbr: 'SPST',
- offset: -5,
- isdst: false,
- text: '(UTC-05:00) Bogota, Lima, Quito',
- utc: [
- 'America/Bogota',
- 'America/Cayman',
- 'America/Coral_Harbour',
- 'America/Eirunepe',
- 'America/Guayaquil',
- 'America/Jamaica',
- 'America/Lima',
- 'America/Panama',
- 'America/Rio_Branco',
- 'Etc/GMT+5',
- ],
- },
- {
- value: 'Eastern Standard Time',
- abbr: 'EST',
- offset: -5,
- isdst: false,
- text: '(UTC-05:00) Eastern Time (US & Canada)',
- utc: [
- 'America/Detroit',
- 'America/Havana',
- 'America/Indiana/Petersburg',
- 'America/Indiana/Vincennes',
- 'America/Indiana/Winamac',
- 'America/Iqaluit',
- 'America/Kentucky/Monticello',
- 'America/Louisville',
- 'America/Montreal',
- 'America/Nassau',
- 'America/New_York',
- 'America/Nipigon',
- 'America/Pangnirtung',
- 'America/Port-au-Prince',
- 'America/Thunder_Bay',
- 'America/Toronto',
- ],
- },
- {
- value: 'Eastern Daylight Time',
- abbr: 'EDT',
- offset: -4,
- isdst: true,
- text: '(UTC-04:00) Eastern Daylight Time (US & Canada)',
- utc: [
- 'America/Detroit',
- 'America/Havana',
- 'America/Indiana/Petersburg',
- 'America/Indiana/Vincennes',
- 'America/Indiana/Winamac',
- 'America/Iqaluit',
- 'America/Kentucky/Monticello',
- 'America/Louisville',
- 'America/Montreal',
- 'America/Nassau',
- 'America/New_York',
- 'America/Nipigon',
- 'America/Pangnirtung',
- 'America/Port-au-Prince',
- 'America/Thunder_Bay',
- 'America/Toronto',
- ],
- },
- {
- value: 'US Eastern Standard Time',
- abbr: 'UEDT',
- offset: -5,
- isdst: false,
- text: '(UTC-05:00) Indiana (East)',
- utc: ['America/Indiana/Marengo', 'America/Indiana/Vevay', 'America/Indianapolis'],
- },
- {
- value: 'Venezuela Standard Time',
- abbr: 'VST',
- offset: -4.5,
- isdst: false,
- text: '(UTC-04:30) Caracas',
- utc: ['America/Caracas'],
- },
- {
- value: 'Paraguay Standard Time',
- abbr: 'PYT',
- offset: -4,
- isdst: false,
- text: '(UTC-04:00) Asuncion',
- utc: ['America/Asuncion'],
- },
- {
- value: 'Atlantic Standard Time',
- abbr: 'ADT',
- offset: -3,
- isdst: true,
- text: '(UTC-04:00) Atlantic Time (Canada)',
- utc: [
- 'America/Glace_Bay',
- 'America/Goose_Bay',
- 'America/Halifax',
- 'America/Moncton',
- 'America/Thule',
- 'Atlantic/Bermuda',
- ],
- },
- {
- value: 'Central Brazilian Standard Time',
- abbr: 'CBST',
- offset: -4,
- isdst: false,
- text: '(UTC-04:00) Cuiaba',
- utc: ['America/Campo_Grande', 'America/Cuiaba'],
- },
- {
- value: 'SA Western Standard Time',
- abbr: 'SWST',
- offset: -4,
- isdst: false,
- text: '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan',
- utc: [
- 'America/Anguilla',
- 'America/Antigua',
- 'America/Aruba',
- 'America/Barbados',
- 'America/Blanc-Sablon',
- 'America/Boa_Vista',
- 'America/Curacao',
- 'America/Dominica',
- 'America/Grand_Turk',
- 'America/Grenada',
- 'America/Guadeloupe',
- 'America/Guyana',
- 'America/Kralendijk',
- 'America/La_Paz',
- 'America/Lower_Princes',
- 'America/Manaus',
- 'America/Marigot',
- 'America/Martinique',
- 'America/Montserrat',
- 'America/Port_of_Spain',
- 'America/Porto_Velho',
- 'America/Puerto_Rico',
- 'America/Santo_Domingo',
- 'America/St_Barthelemy',
- 'America/St_Kitts',
- 'America/St_Lucia',
- 'America/St_Thomas',
- 'America/St_Vincent',
- 'America/Tortola',
- 'Etc/GMT+4',
- ],
- },
- {
- value: 'Pacific SA Standard Time',
- abbr: 'PSST',
- offset: -4,
- isdst: false,
- text: '(UTC-04:00) Santiago',
- utc: ['America/Santiago', 'Antarctica/Palmer'],
- },
- {
- value: 'Newfoundland Standard Time',
- abbr: 'NDT',
- offset: -2.5,
- isdst: true,
- text: '(UTC-03:30) Newfoundland',
- utc: ['America/St_Johns'],
- },
- {
- value: 'E. South America Standard Time',
- abbr: 'ESAST',
- offset: -3,
- isdst: false,
- text: '(UTC-03:00) Brasilia',
- utc: ['America/Sao_Paulo'],
- },
- {
- value: 'Argentina Standard Time',
- abbr: 'AST',
- offset: -3,
- isdst: false,
- text: '(UTC-03:00) Buenos Aires',
- utc: [
- 'America/Argentina/La_Rioja',
- 'America/Argentina/Rio_Gallegos',
- 'America/Argentina/Salta',
- 'America/Argentina/San_Juan',
- 'America/Argentina/San_Luis',
- 'America/Argentina/Tucuman',
- 'America/Argentina/Ushuaia',
- 'America/Buenos_Aires',
- 'America/Catamarca',
- 'America/Cordoba',
- 'America/Jujuy',
- 'America/Mendoza',
- ],
- },
- {
- value: 'SA Eastern Standard Time',
- abbr: 'SEST',
- offset: -3,
- isdst: false,
- text: '(UTC-03:00) Cayenne, Fortaleza',
- utc: [
- 'America/Araguaina',
- 'America/Belem',
- 'America/Cayenne',
- 'America/Fortaleza',
- 'America/Maceio',
- 'America/Paramaribo',
- 'America/Recife',
- 'America/Santarem',
- 'Antarctica/Rothera',
- 'Atlantic/Stanley',
- 'Etc/GMT+3',
- ],
- },
- {
- value: 'Greenland Standard Time',
- abbr: 'GDT',
- offset: -3,
- isdst: true,
- text: '(UTC-03:00) Greenland',
- utc: ['America/Godthab'],
- },
- {
- value: 'Montevideo Standard Time',
- abbr: 'MST',
- offset: -3,
- isdst: false,
- text: '(UTC-03:00) Montevideo',
- utc: ['America/Montevideo'],
- },
- {
- value: 'Bahia Standard Time',
- abbr: 'BST',
- offset: -3,
- isdst: false,
- text: '(UTC-03:00) Salvador',
- utc: ['America/Bahia'],
- },
- {
- value: 'UTC-02',
- abbr: 'U',
- offset: -2,
- isdst: false,
- text: '(UTC-02:00) Coordinated Universal Time-02',
- utc: ['America/Noronha', 'Atlantic/South_Georgia', 'Etc/GMT+2'],
- },
- {
- value: 'Azores Standard Time',
- abbr: 'ADT',
- offset: 0,
- isdst: true,
- text: '(UTC-01:00) Azores',
- utc: ['America/Scoresbysund', 'Atlantic/Azores'],
- },
- {
- value: 'Cape Verde Standard Time',
- abbr: 'CVST',
- offset: -1,
- isdst: false,
- text: '(UTC-01:00) Cape Verde Is.',
- utc: ['Atlantic/Cape_Verde', 'Etc/GMT+1'],
- },
- {
- value: 'Morocco Standard Time',
- abbr: 'MDT',
- offset: 1,
- isdst: true,
- text: '(UTC) Casablanca',
- utc: ['Africa/Casablanca', 'Africa/El_Aaiun'],
- },
- {
- value: 'UTC',
- abbr: 'UTC',
- offset: 0,
- isdst: false,
- text: '(UTC) Coordinated Universal Time',
- utc: ['America/Danmarkshavn', 'Etc/GMT'],
- },
- {
- value: 'British Summer Time',
- abbr: 'BST',
- offset: 1,
- isdst: true,
- text: '(UTC+01:00) Edinburgh, London',
- utc: ['Europe/Isle_of_Man', 'Europe/Guernsey', 'Europe/Jersey', 'Europe/London'],
- },
- {
- value: 'GMT Standard Time',
- abbr: 'GDT',
- offset: 1,
- isdst: true,
- text: '(UTC) Dublin, Lisbon',
- utc: [
- 'Atlantic/Canary',
- 'Atlantic/Faeroe',
- 'Atlantic/Madeira',
- 'Europe/Dublin',
- 'Europe/Lisbon',
- ],
- },
- {
- value: 'Greenwich Standard Time',
- abbr: 'GST',
- offset: 0,
- isdst: false,
- text: '(UTC) Monrovia, Reykjavik',
- utc: [
- 'Africa/Abidjan',
- 'Africa/Accra',
- 'Africa/Bamako',
- 'Africa/Banjul',
- 'Africa/Bissau',
- 'Africa/Conakry',
- 'Africa/Dakar',
- 'Africa/Freetown',
- 'Africa/Lome',
- 'Africa/Monrovia',
- 'Africa/Nouakchott',
- 'Africa/Ouagadougou',
- 'Africa/Sao_Tome',
- 'Atlantic/Reykjavik',
- 'Atlantic/St_Helena',
- ],
- },
- {
- value: 'W. Europe Standard Time',
- abbr: 'WEDT',
- offset: 2,
- isdst: true,
- text: '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna',
- utc: [
- 'Arctic/Longyearbyen',
- 'Europe/Amsterdam',
- 'Europe/Andorra',
- 'Europe/Berlin',
- 'Europe/Busingen',
- 'Europe/Gibraltar',
- 'Europe/Luxembourg',
- 'Europe/Malta',
- 'Europe/Monaco',
- 'Europe/Oslo',
- 'Europe/Rome',
- 'Europe/San_Marino',
- 'Europe/Stockholm',
- 'Europe/Vaduz',
- 'Europe/Vatican',
- 'Europe/Vienna',
- 'Europe/Zurich',
- ],
- },
- {
- value: 'Central Europe Standard Time',
- abbr: 'CEDT',
- offset: 2,
- isdst: true,
- text: '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague',
- utc: [
- 'Europe/Belgrade',
- 'Europe/Bratislava',
- 'Europe/Budapest',
- 'Europe/Ljubljana',
- 'Europe/Podgorica',
- 'Europe/Prague',
- 'Europe/Tirane',
- ],
- },
- {
- value: 'Romance Standard Time',
- abbr: 'RDT',
- offset: 2,
- isdst: true,
- text: '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris',
- utc: ['Africa/Ceuta', 'Europe/Brussels', 'Europe/Copenhagen', 'Europe/Madrid', 'Europe/Paris'],
- },
- {
- value: 'Central European Standard Time',
- abbr: 'CEDT',
- offset: 2,
- isdst: true,
- text: '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb',
- utc: ['Europe/Sarajevo', 'Europe/Skopje', 'Europe/Warsaw', 'Europe/Zagreb'],
- },
- {
- value: 'W. Central Africa Standard Time',
- abbr: 'WCAST',
- offset: 1,
- isdst: false,
- text: '(UTC+01:00) West Central Africa',
- utc: [
- 'Africa/Algiers',
- 'Africa/Bangui',
- 'Africa/Brazzaville',
- 'Africa/Douala',
- 'Africa/Kinshasa',
- 'Africa/Lagos',
- 'Africa/Libreville',
- 'Africa/Luanda',
- 'Africa/Malabo',
- 'Africa/Ndjamena',
- 'Africa/Niamey',
- 'Africa/Porto-Novo',
- 'Africa/Tunis',
- 'Etc/GMT-1',
- ],
- },
- {
- value: 'Namibia Standard Time',
- abbr: 'NST',
- offset: 1,
- isdst: false,
- text: '(UTC+01:00) Windhoek',
- utc: ['Africa/Windhoek'],
- },
- {
- value: 'GTB Standard Time',
- abbr: 'GDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) Athens, Bucharest',
- utc: ['Asia/Nicosia', 'Europe/Athens', 'Europe/Bucharest', 'Europe/Chisinau'],
- },
- {
- value: 'Middle East Standard Time',
- abbr: 'MEDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) Beirut',
- utc: ['Asia/Beirut'],
- },
- {
- value: 'Egypt Standard Time',
- abbr: 'EST',
- offset: 2,
- isdst: false,
- text: '(UTC+02:00) Cairo',
- utc: ['Africa/Cairo'],
- },
- {
- value: 'Syria Standard Time',
- abbr: 'SDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) Damascus',
- utc: ['Asia/Damascus'],
- },
- {
- value: 'E. Europe Standard Time',
- abbr: 'EEDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) E. Europe',
- utc: [
- 'Asia/Nicosia',
- 'Europe/Athens',
- 'Europe/Bucharest',
- 'Europe/Chisinau',
- 'Europe/Helsinki',
- 'Europe/Kiev',
- 'Europe/Mariehamn',
- 'Europe/Nicosia',
- 'Europe/Riga',
- 'Europe/Sofia',
- 'Europe/Tallinn',
- 'Europe/Uzhgorod',
- 'Europe/Vilnius',
- 'Europe/Zaporozhye',
- ],
- },
- {
- value: 'South Africa Standard Time',
- abbr: 'SAST',
- offset: 2,
- isdst: false,
- text: '(UTC+02:00) Harare, Pretoria',
- utc: [
- 'Africa/Blantyre',
- 'Africa/Bujumbura',
- 'Africa/Gaborone',
- 'Africa/Harare',
- 'Africa/Johannesburg',
- 'Africa/Kigali',
- 'Africa/Lubumbashi',
- 'Africa/Lusaka',
- 'Africa/Maputo',
- 'Africa/Maseru',
- 'Africa/Mbabane',
- 'Etc/GMT-2',
- ],
- },
- {
- value: 'FLE Standard Time',
- abbr: 'FDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius',
- utc: [
- 'Europe/Helsinki',
- 'Europe/Kiev',
- 'Europe/Mariehamn',
- 'Europe/Riga',
- 'Europe/Sofia',
- 'Europe/Tallinn',
- 'Europe/Uzhgorod',
- 'Europe/Vilnius',
- 'Europe/Zaporozhye',
- ],
- },
- {
- value: 'Turkey Standard Time',
- abbr: 'TDT',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Istanbul',
- utc: ['Europe/Istanbul'],
- },
- {
- value: 'Israel Standard Time',
- abbr: 'JDT',
- offset: 3,
- isdst: true,
- text: '(UTC+02:00) Jerusalem',
- utc: ['Asia/Jerusalem'],
- },
- {
- value: 'Libya Standard Time',
- abbr: 'LST',
- offset: 2,
- isdst: false,
- text: '(UTC+02:00) Tripoli',
- utc: ['Africa/Tripoli'],
- },
- {
- value: 'Jordan Standard Time',
- abbr: 'JST',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Amman',
- utc: ['Asia/Amman'],
- },
- {
- value: 'Arabic Standard Time',
- abbr: 'AST',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Baghdad',
- utc: ['Asia/Baghdad'],
- },
- {
- value: 'Kaliningrad Standard Time',
- abbr: 'KST',
- offset: 3,
- isdst: false,
- text: '(UTC+02:00) Kaliningrad',
- utc: ['Europe/Kaliningrad'],
- },
- {
- value: 'Arab Standard Time',
- abbr: 'AST',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Kuwait, Riyadh',
- utc: ['Asia/Aden', 'Asia/Bahrain', 'Asia/Kuwait', 'Asia/Qatar', 'Asia/Riyadh'],
- },
- {
- value: 'E. Africa Standard Time',
- abbr: 'EAST',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Nairobi',
- utc: [
- 'Africa/Addis_Ababa',
- 'Africa/Asmera',
- 'Africa/Dar_es_Salaam',
- 'Africa/Djibouti',
- 'Africa/Juba',
- 'Africa/Kampala',
- 'Africa/Khartoum',
- 'Africa/Mogadishu',
- 'Africa/Nairobi',
- 'Antarctica/Syowa',
- 'Etc/GMT-3',
- 'Indian/Antananarivo',
- 'Indian/Comoro',
- 'Indian/Mayotte',
- ],
- },
- {
- value: 'Moscow Standard Time',
- abbr: 'MSK',
- offset: 3,
- isdst: false,
- text: '(UTC+03:00) Moscow, St. Petersburg, Volgograd, Minsk',
- utc: ['Europe/Kirov', 'Europe/Moscow', 'Europe/Simferopol', 'Europe/Volgograd', 'Europe/Minsk'],
- },
- {
- value: 'Samara Time',
- abbr: 'SAMT',
- offset: 4,
- isdst: false,
- text: '(UTC+04:00) Samara, Ulyanovsk, Saratov',
- utc: ['Europe/Astrakhan', 'Europe/Samara', 'Europe/Ulyanovsk'],
- },
- {
- value: 'Iran Standard Time',
- abbr: 'IDT',
- offset: 4.5,
- isdst: true,
- text: '(UTC+03:30) Tehran',
- utc: ['Asia/Tehran'],
- },
- {
- value: 'Arabian Standard Time',
- abbr: 'AST',
- offset: 4,
- isdst: false,
- text: '(UTC+04:00) Abu Dhabi, Muscat',
- utc: ['Asia/Dubai', 'Asia/Muscat', 'Etc/GMT-4'],
- },
- {
- value: 'Azerbaijan Standard Time',
- abbr: 'ADT',
- offset: 5,
- isdst: true,
- text: '(UTC+04:00) Baku',
- utc: ['Asia/Baku'],
- },
- {
- value: 'Mauritius Standard Time',
- abbr: 'MST',
- offset: 4,
- isdst: false,
- text: '(UTC+04:00) Port Louis',
- utc: ['Indian/Mahe', 'Indian/Mauritius', 'Indian/Reunion'],
- },
- {
- value: 'Georgian Standard Time',
- abbr: 'GET',
- offset: 4,
- isdst: false,
- text: '(UTC+04:00) Tbilisi',
- utc: ['Asia/Tbilisi'],
- },
- {
- value: 'Caucasus Standard Time',
- abbr: 'CST',
- offset: 4,
- isdst: false,
- text: '(UTC+04:00) Yerevan',
- utc: ['Asia/Yerevan'],
- },
- {
- value: 'Afghanistan Standard Time',
- abbr: 'AST',
- offset: 4.5,
- isdst: false,
- text: '(UTC+04:30) Kabul',
- utc: ['Asia/Kabul'],
- },
- {
- value: 'West Asia Standard Time',
- abbr: 'WAST',
- offset: 5,
- isdst: false,
- text: '(UTC+05:00) Ashgabat, Tashkent',
- utc: [
- 'Antarctica/Mawson',
- 'Asia/Aqtau',
- 'Asia/Aqtobe',
- 'Asia/Ashgabat',
- 'Asia/Dushanbe',
- 'Asia/Oral',
- 'Asia/Samarkand',
- 'Asia/Tashkent',
- 'Etc/GMT-5',
- 'Indian/Kerguelen',
- 'Indian/Maldives',
- ],
- },
- {
- value: 'Yekaterinburg Time',
- abbr: 'YEKT',
- offset: 5,
- isdst: false,
- text: '(UTC+05:00) Yekaterinburg',
- utc: ['Asia/Yekaterinburg'],
- },
- {
- value: 'Pakistan Standard Time',
- abbr: 'PKT',
- offset: 5,
- isdst: false,
- text: '(UTC+05:00) Islamabad, Karachi',
- utc: ['Asia/Karachi'],
- },
- {
- value: 'India Standard Time',
- abbr: 'IST',
- offset: 5.5,
- isdst: false,
- text: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi',
- utc: ['Asia/Kolkata', 'Asia/Calcutta'],
- },
- {
- value: 'Sri Lanka Standard Time',
- abbr: 'SLST',
- offset: 5.5,
- isdst: false,
- text: '(UTC+05:30) Sri Jayawardenepura',
- utc: ['Asia/Colombo'],
- },
- {
- value: 'Nepal Standard Time',
- abbr: 'NST',
- offset: 5.75,
- isdst: false,
- text: '(UTC+05:45) Kathmandu',
- utc: ['Asia/Kathmandu'],
- },
- {
- value: 'Central Asia Standard Time',
- abbr: 'CAST',
- offset: 6,
- isdst: false,
- text: '(UTC+06:00) Nur-Sultan (Astana)',
- utc: [
- 'Antarctica/Vostok',
- 'Asia/Almaty',
- 'Asia/Bishkek',
- 'Asia/Qyzylorda',
- 'Asia/Urumqi',
- 'Etc/GMT-6',
- 'Indian/Chagos',
- ],
- },
- {
- value: 'Bangladesh Standard Time',
- abbr: 'BST',
- offset: 6,
- isdst: false,
- text: '(UTC+06:00) Dhaka',
- utc: ['Asia/Dhaka', 'Asia/Thimphu'],
- },
- {
- value: 'Myanmar Standard Time',
- abbr: 'MST',
- offset: 6.5,
- isdst: false,
- text: '(UTC+06:30) Yangon (Rangoon)',
- utc: ['Asia/Rangoon', 'Indian/Cocos'],
- },
- {
- value: 'SE Asia Standard Time',
- abbr: 'SAST',
- offset: 7,
- isdst: false,
- text: '(UTC+07:00) Bangkok, Hanoi, Jakarta',
- utc: [
- 'Antarctica/Davis',
- 'Asia/Bangkok',
- 'Asia/Hovd',
- 'Asia/Jakarta',
- 'Asia/Phnom_Penh',
- 'Asia/Pontianak',
- 'Asia/Saigon',
- 'Asia/Vientiane',
- 'Etc/GMT-7',
- 'Indian/Christmas',
- ],
- },
- {
- value: 'N. Central Asia Standard Time',
- abbr: 'NCAST',
- offset: 7,
- isdst: false,
- text: '(UTC+07:00) Novosibirsk',
- utc: ['Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk'],
- },
- {
- value: 'China Standard Time',
- abbr: 'CST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi',
- utc: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'],
- },
- {
- value: 'North Asia Standard Time',
- abbr: 'NAST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Krasnoyarsk',
- utc: ['Asia/Krasnoyarsk'],
- },
- {
- value: 'Singapore Standard Time',
- abbr: 'MPST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Kuala Lumpur, Singapore',
- utc: [
- 'Asia/Brunei',
- 'Asia/Kuala_Lumpur',
- 'Asia/Kuching',
- 'Asia/Makassar',
- 'Asia/Manila',
- 'Asia/Singapore',
- 'Etc/GMT-8',
- ],
- },
- {
- value: 'W. Australia Standard Time',
- abbr: 'WAST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Perth',
- utc: ['Antarctica/Casey', 'Australia/Perth'],
- },
- {
- value: 'Taipei Standard Time',
- abbr: 'TST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Taipei',
- utc: ['Asia/Taipei'],
- },
- {
- value: 'Ulaanbaatar Standard Time',
- abbr: 'UST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Ulaanbaatar',
- utc: ['Asia/Choibalsan', 'Asia/Ulaanbaatar'],
- },
- {
- value: 'North Asia East Standard Time',
- abbr: 'NAEST',
- offset: 8,
- isdst: false,
- text: '(UTC+08:00) Irkutsk',
- utc: ['Asia/Irkutsk'],
- },
- {
- value: 'Japan Standard Time',
- abbr: 'JST',
- offset: 9,
- isdst: false,
- text: '(UTC+09:00) Osaka, Sapporo, Tokyo',
- utc: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau'],
- },
- {
- value: 'Korea Standard Time',
- abbr: 'KST',
- offset: 9,
- isdst: false,
- text: '(UTC+09:00) Seoul',
- utc: ['Asia/Pyongyang', 'Asia/Seoul'],
- },
- {
- value: 'Cen. Australia Standard Time',
- abbr: 'CAST',
- offset: 9.5,
- isdst: false,
- text: '(UTC+09:30) Adelaide',
- utc: ['Australia/Adelaide', 'Australia/Broken_Hill'],
- },
- {
- value: 'AUS Central Standard Time',
- abbr: 'ACST',
- offset: 9.5,
- isdst: false,
- text: '(UTC+09:30) Darwin',
- utc: ['Australia/Darwin'],
- },
- {
- value: 'E. Australia Standard Time',
- abbr: 'EAST',
- offset: 10,
- isdst: false,
- text: '(UTC+10:00) Brisbane',
- utc: ['Australia/Brisbane', 'Australia/Lindeman'],
- },
- {
- value: 'AUS Eastern Standard Time',
- abbr: 'AEST',
- offset: 10,
- isdst: false,
- text: '(UTC+10:00) Canberra, Melbourne, Sydney',
- utc: ['Australia/Melbourne', 'Australia/Sydney'],
- },
- {
- value: 'West Pacific Standard Time',
- abbr: 'WPST',
- offset: 10,
- isdst: false,
- text: '(UTC+10:00) Guam, Port Moresby',
- utc: [
- 'Antarctica/DumontDUrville',
- 'Etc/GMT-10',
- 'Pacific/Guam',
- 'Pacific/Port_Moresby',
- 'Pacific/Saipan',
- 'Pacific/Truk',
- ],
- },
- {
- value: 'Tasmania Standard Time',
- abbr: 'TST',
- offset: 10,
- isdst: false,
- text: '(UTC+10:00) Hobart',
- utc: ['Australia/Currie', 'Australia/Hobart'],
- },
- {
- value: 'Yakutsk Standard Time',
- abbr: 'YST',
- offset: 9,
- isdst: false,
- text: '(UTC+09:00) Yakutsk',
- utc: ['Asia/Chita', 'Asia/Khandyga', 'Asia/Yakutsk'],
- },
- {
- value: 'Central Pacific Standard Time',
- abbr: 'CPST',
- offset: 11,
- isdst: false,
- text: '(UTC+11:00) Solomon Is., New Caledonia',
- utc: [
- 'Antarctica/Macquarie',
- 'Etc/GMT-11',
- 'Pacific/Efate',
- 'Pacific/Guadalcanal',
- 'Pacific/Kosrae',
- 'Pacific/Noumea',
- 'Pacific/Ponape',
- ],
- },
- {
- value: 'Vladivostok Standard Time',
- abbr: 'VST',
- offset: 11,
- isdst: false,
- text: '(UTC+11:00) Vladivostok',
- utc: ['Asia/Sakhalin', 'Asia/Ust-Nera', 'Asia/Vladivostok'],
- },
- {
- value: 'New Zealand Standard Time',
- abbr: 'NZST',
- offset: 12,
- isdst: false,
- text: '(UTC+12:00) Auckland, Wellington',
- utc: ['Antarctica/McMurdo', 'Pacific/Auckland'],
- },
- {
- value: 'UTC+12',
- abbr: 'U',
- offset: 12,
- isdst: false,
- text: '(UTC+12:00) Coordinated Universal Time+12',
- utc: [
- 'Etc/GMT-12',
- 'Pacific/Funafuti',
- 'Pacific/Kwajalein',
- 'Pacific/Majuro',
- 'Pacific/Nauru',
- 'Pacific/Tarawa',
- 'Pacific/Wake',
- 'Pacific/Wallis',
- ],
- },
- {
- value: 'Fiji Standard Time',
- abbr: 'FST',
- offset: 12,
- isdst: false,
- text: '(UTC+12:00) Fiji',
- utc: ['Pacific/Fiji'],
- },
- {
- value: 'Magadan Standard Time',
- abbr: 'MST',
- offset: 12,
- isdst: false,
- text: '(UTC+12:00) Magadan',
- utc: ['Asia/Anadyr', 'Asia/Kamchatka', 'Asia/Magadan', 'Asia/Srednekolymsk'],
- },
- {
- value: 'Kamchatka Standard Time',
- abbr: 'KDT',
- offset: 13,
- isdst: true,
- text: '(UTC+12:00) Petropavlovsk-Kamchatsky - Old',
- utc: ['Asia/Kamchatka'],
- },
- {
- value: 'Tonga Standard Time',
- abbr: 'TST',
- offset: 13,
- isdst: false,
- text: "(UTC+13:00) Nuku'alofa",
- utc: ['Etc/GMT-13', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Tongatapu'],
- },
- {
- value: 'Samoa Standard Time',
- abbr: 'SST',
- offset: 13,
- isdst: false,
- text: '(UTC+13:00) Samoa',
- utc: ['Pacific/Apia'],
- },
-]
+// Re-export from the shared location. The full timezone catalog now lives in
+// `lib/constants/timezones.ts` so it can be consumed outside of PITR.
+export { ALL_TIMEZONES } from '@/lib/constants/timezones'
diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx
index 509fa74f134..e3512b5ead0 100644
--- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx
+++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx
@@ -1,4 +1,3 @@
-import { format } from 'date-fns'
import { User } from 'lucide-react'
import { cn } from 'ui'
@@ -9,6 +8,27 @@ import { getLevelLabel } from './UnifiedLogs.utils'
import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
import { DataTableFilterField, Option } from '@/components/ui/DataTable/DataTable.types'
import { getLevelColor } from '@/components/ui/DataTable/DataTable.utils'
+import { useFormatDateTime } from '@/lib/datetime'
+
+const DateCell = (props: { date: ColumnSchema['date'] }) => {
+ const formatDateTime = useFormatDateTime()
+ const month = formatDateTime(props.date, 'MMM')
+ const day = formatDateTime(props.date, 'DD')
+ const year = formatDateTime(props.date, 'YYYY')
+ const time = formatDateTime(props.date, 'HH:mm:ss')
+
+ return (
+
+ {month}
+ ·
+ {day}
+ ·
+ {year}
+ ·
+ {time}
+
+ )
+}
// instead of filterFields, maybe just 'fields' with a filterDisabled prop?
// that way, we could have 'message' or 'headers' field with label and value as well as type!
@@ -127,25 +147,7 @@ export const sheetFields = [
id: 'date',
label: 'Date',
type: 'timerange',
- component: (props) => {
- const date = new Date(props.date)
- const month = format(date, 'LLL')
- const day = format(date, 'dd')
- const year = format(date, 'y')
- const time = format(date, 'HH:mm:ss')
-
- return (
-
- {month}
- ·
- {day}
- ·
- {year}
- ·
- {time}
-
- )
- },
+ component: DateCell,
skeletonClassName: 'w-36',
},
{
diff --git a/apps/studio/components/interfaces/UserDropdown.tsx b/apps/studio/components/interfaces/UserDropdown.tsx
index 599bd1d8fb1..d31d90dec39 100644
--- a/apps/studio/components/interfaces/UserDropdown.tsx
+++ b/apps/studio/components/interfaces/UserDropdown.tsx
@@ -1,3 +1,4 @@
+import { useFlag } from 'common'
import { FlaskConical, Loader2, ScrollText, Settings } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
@@ -19,6 +20,7 @@ import {
import { ButtonTooltip } from '../ui/ButtonTooltip'
import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
+import { TimezoneDropdown } from './UserDropdown/TimezoneDropdown'
import { ProfileImage } from '@/components/ui/ProfileImage'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from '@/lib/constants'
@@ -37,6 +39,7 @@ export function UserDropdown({
const { theme, setTheme } = useTheme()
const appStateSnapshot = useAppStateSnapshot()
const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
+ const timezonePickerEnabled = useFlag('timezonePicker')
const { username, avatarUrl, primaryEmail, isLoading } = useProfileNameAndPicture()
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
@@ -144,6 +147,14 @@ export function UserDropdown({
))}
+ {timezonePickerEnabled && (
+ <>
+
+
+
+
+ >
+ )}
{IS_PLATFORM && (
<>
diff --git a/apps/studio/components/interfaces/UserDropdown/TimezoneDropdown.tsx b/apps/studio/components/interfaces/UserDropdown/TimezoneDropdown.tsx
new file mode 100644
index 00000000000..f6e7cf989be
--- /dev/null
+++ b/apps/studio/components/interfaces/UserDropdown/TimezoneDropdown.tsx
@@ -0,0 +1,113 @@
+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 (
+
+
+
+ Timezone
+
+ {isAutoDetected ? `Auto (${timezone})` : triggerLabel}
+
+
+
+
+
+
+
+
+ No timezones found
+
+
+ handleSelect('')}
+ >
+
+ Auto detect
+ {browserTimezone}
+
+
+
+ {TIMEZONES_BY_IANA.map((entry) => {
+ const ianaName = entry.utc[0]
+ const isSelected = !isAutoDetected && storedTimezone === ianaName
+ return (
+ handleSelect(ianaName)}
+ >
+ {entry.text}
+
+
+ )
+ })}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/lib/constants/timezones.test.ts b/apps/studio/lib/constants/timezones.test.ts
new file mode 100644
index 00000000000..69bc7ea7ca8
--- /dev/null
+++ b/apps/studio/lib/constants/timezones.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest'
+
+import { ALL_TIMEZONES, findTimezoneByIana, TIMEZONES_BY_IANA } from '@/lib/constants/timezones'
+
+describe('TIMEZONES_BY_IANA', () => {
+ it('produces one row per primary IANA name', () => {
+ const ianas = TIMEZONES_BY_IANA.map((entry) => entry.utc[0])
+ expect(new Set(ianas).size).toBe(ianas.length)
+ })
+
+ it('prefers the standard-time row when multiple catalog rows share a primary IANA', () => {
+ // ALL_TIMEZONES has both PDT (isdst: true) and PST (isdst: false) pointing
+ // at America/Los_Angeles. The deduped view should pick the standard one
+ // so the picker label doesn't flip on DST changes.
+ const collisions = ALL_TIMEZONES.filter((entry) => entry.utc[0] === 'America/Los_Angeles')
+ expect(collisions.length).toBeGreaterThan(1)
+
+ const winner = TIMEZONES_BY_IANA.find((entry) => entry.utc[0] === 'America/Los_Angeles')
+ expect(winner).toBeDefined()
+ expect(winner!.isdst).toBe(false)
+ })
+
+ it('preserves entries that have no collision', () => {
+ // Most rows have a unique primary IANA. The UTC catalog row's primary is
+ // 'America/Danmarkshavn' and survives the dedupe pass unchanged.
+ const utcRow = TIMEZONES_BY_IANA.find((entry) => entry.value === 'UTC')
+ expect(utcRow?.text).toContain('Coordinated Universal Time')
+ expect(utcRow?.utc[0]).toBe('America/Danmarkshavn')
+ })
+})
+
+describe('findTimezoneByIana', () => {
+ it('matches the entry by its primary IANA name', () => {
+ const entry = findTimezoneByIana('America/Danmarkshavn')
+ expect(entry?.text).toContain('Coordinated Universal Time')
+ })
+
+ it('matches the entry by any of its secondary IANA names', () => {
+ // 'Asia/Tokyo' is one of the IANA aliases on the JST row whose primary
+ // IANA is 'Asia/Dili'. Lookup must walk the full alias list.
+ const entry = findTimezoneByIana('Asia/Tokyo')
+ expect(entry?.utc).toContain('Asia/Tokyo')
+ })
+
+ it('returns undefined for an unknown IANA name', () => {
+ expect(findTimezoneByIana('Not/A/Real_Zone')).toBeUndefined()
+ })
+})
diff --git a/apps/studio/lib/constants/timezones.ts b/apps/studio/lib/constants/timezones.ts
new file mode 100644
index 00000000000..218b45719e0
--- /dev/null
+++ b/apps/studio/lib/constants/timezones.ts
@@ -0,0 +1,1221 @@
+// https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json
+export interface Timezone {
+ value: string
+ abbr: string
+ offset: number
+ isdst: boolean
+ text: string
+ utc: string[]
+}
+
+export const ALL_TIMEZONES: Timezone[] = [
+ {
+ value: 'Dateline Standard Time',
+ abbr: 'DST',
+ offset: -12,
+ isdst: false,
+ text: '(UTC-12:00) International Date Line West',
+ utc: ['Etc/GMT+12'],
+ },
+ {
+ value: 'UTC-11',
+ abbr: 'U',
+ offset: -11,
+ isdst: false,
+ text: '(UTC-11:00) Coordinated Universal Time-11',
+ utc: ['Etc/GMT+11', 'Pacific/Midway', 'Pacific/Niue', 'Pacific/Pago_Pago'],
+ },
+ {
+ value: 'Hawaiian Standard Time',
+ abbr: 'HST',
+ offset: -10,
+ isdst: false,
+ text: '(UTC-10:00) Hawaii',
+ utc: [
+ 'Etc/GMT+10',
+ 'Pacific/Honolulu',
+ 'Pacific/Johnston',
+ 'Pacific/Rarotonga',
+ 'Pacific/Tahiti',
+ ],
+ },
+ {
+ value: 'Alaskan Standard Time',
+ abbr: 'AKDT',
+ offset: -8,
+ isdst: true,
+ text: '(UTC-09:00) Alaska',
+ utc: [
+ 'America/Anchorage',
+ 'America/Juneau',
+ 'America/Nome',
+ 'America/Sitka',
+ 'America/Yakutat',
+ ],
+ },
+ {
+ value: 'Pacific Standard Time (Mexico)',
+ abbr: 'PDT',
+ offset: -7,
+ isdst: true,
+ text: '(UTC-08:00) Baja California',
+ utc: ['America/Santa_Isabel'],
+ },
+ {
+ value: 'Pacific Daylight Time',
+ abbr: 'PDT',
+ offset: -7,
+ isdst: true,
+ text: '(UTC-07:00) Pacific Daylight Time (US & Canada)',
+ utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver'],
+ },
+ {
+ value: 'Pacific Standard Time',
+ abbr: 'PST',
+ offset: -8,
+ isdst: false,
+ text: '(UTC-08:00) Pacific Standard Time (US & Canada)',
+ utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver', 'PST8PDT'],
+ },
+ {
+ value: 'US Mountain Standard Time',
+ abbr: 'UMST',
+ offset: -7,
+ isdst: false,
+ text: '(UTC-07:00) Arizona',
+ utc: [
+ 'America/Creston',
+ 'America/Dawson',
+ 'America/Dawson_Creek',
+ 'America/Hermosillo',
+ 'America/Phoenix',
+ 'America/Whitehorse',
+ 'Etc/GMT+7',
+ ],
+ },
+ {
+ value: 'Mountain Standard Time (Mexico)',
+ abbr: 'MDT',
+ offset: -6,
+ isdst: true,
+ text: '(UTC-07:00) Chihuahua, La Paz, Mazatlan',
+ utc: ['America/Chihuahua', 'America/Mazatlan'],
+ },
+ {
+ value: 'Mountain Standard Time',
+ abbr: 'MDT',
+ offset: -6,
+ isdst: true,
+ text: '(UTC-07:00) Mountain Time (US & Canada)',
+ utc: [
+ 'America/Boise',
+ 'America/Cambridge_Bay',
+ 'America/Denver',
+ 'America/Edmonton',
+ 'America/Inuvik',
+ 'America/Ojinaga',
+ 'America/Yellowknife',
+ 'MST7MDT',
+ ],
+ },
+ {
+ value: 'Central America Standard Time',
+ abbr: 'CAST',
+ offset: -6,
+ isdst: false,
+ text: '(UTC-06:00) Central America',
+ utc: [
+ 'America/Belize',
+ 'America/Costa_Rica',
+ 'America/El_Salvador',
+ 'America/Guatemala',
+ 'America/Managua',
+ 'America/Tegucigalpa',
+ 'Etc/GMT+6',
+ 'Pacific/Galapagos',
+ ],
+ },
+ {
+ value: 'Central Standard Time',
+ abbr: 'CDT',
+ offset: -5,
+ isdst: true,
+ text: '(UTC-06:00) Central Time (US & Canada)',
+ utc: [
+ 'America/Chicago',
+ 'America/Indiana/Knox',
+ 'America/Indiana/Tell_City',
+ 'America/Matamoros',
+ 'America/Menominee',
+ 'America/North_Dakota/Beulah',
+ 'America/North_Dakota/Center',
+ 'America/North_Dakota/New_Salem',
+ 'America/Rainy_River',
+ 'America/Rankin_Inlet',
+ 'America/Resolute',
+ 'America/Winnipeg',
+ 'CST6CDT',
+ ],
+ },
+ {
+ value: 'Central Standard Time (Mexico)',
+ abbr: 'CDT',
+ offset: -5,
+ isdst: true,
+ text: '(UTC-06:00) Guadalajara, Mexico City, Monterrey',
+ utc: [
+ 'America/Bahia_Banderas',
+ 'America/Cancun',
+ 'America/Merida',
+ 'America/Mexico_City',
+ 'America/Monterrey',
+ ],
+ },
+ {
+ value: 'Canada Central Standard Time',
+ abbr: 'CCST',
+ offset: -6,
+ isdst: false,
+ text: '(UTC-06:00) Saskatchewan',
+ utc: ['America/Regina', 'America/Swift_Current'],
+ },
+ {
+ value: 'SA Pacific Standard Time',
+ abbr: 'SPST',
+ offset: -5,
+ isdst: false,
+ text: '(UTC-05:00) Bogota, Lima, Quito',
+ utc: [
+ 'America/Bogota',
+ 'America/Cayman',
+ 'America/Coral_Harbour',
+ 'America/Eirunepe',
+ 'America/Guayaquil',
+ 'America/Jamaica',
+ 'America/Lima',
+ 'America/Panama',
+ 'America/Rio_Branco',
+ 'Etc/GMT+5',
+ ],
+ },
+ {
+ value: 'Eastern Standard Time',
+ abbr: 'EST',
+ offset: -5,
+ isdst: false,
+ text: '(UTC-05:00) Eastern Time (US & Canada)',
+ utc: [
+ 'America/Detroit',
+ 'America/Havana',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Iqaluit',
+ 'America/Kentucky/Monticello',
+ 'America/Louisville',
+ 'America/Montreal',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Pangnirtung',
+ 'America/Port-au-Prince',
+ 'America/Thunder_Bay',
+ 'America/Toronto',
+ ],
+ },
+ {
+ value: 'Eastern Daylight Time',
+ abbr: 'EDT',
+ offset: -4,
+ isdst: true,
+ text: '(UTC-04:00) Eastern Daylight Time (US & Canada)',
+ utc: [
+ 'America/Detroit',
+ 'America/Havana',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Iqaluit',
+ 'America/Kentucky/Monticello',
+ 'America/Louisville',
+ 'America/Montreal',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Pangnirtung',
+ 'America/Port-au-Prince',
+ 'America/Thunder_Bay',
+ 'America/Toronto',
+ ],
+ },
+ {
+ value: 'US Eastern Standard Time',
+ abbr: 'UEDT',
+ offset: -5,
+ isdst: false,
+ text: '(UTC-05:00) Indiana (East)',
+ utc: ['America/Indiana/Marengo', 'America/Indiana/Vevay', 'America/Indianapolis'],
+ },
+ {
+ value: 'Venezuela Standard Time',
+ abbr: 'VST',
+ offset: -4.5,
+ isdst: false,
+ text: '(UTC-04:30) Caracas',
+ utc: ['America/Caracas'],
+ },
+ {
+ value: 'Paraguay Standard Time',
+ abbr: 'PYT',
+ offset: -4,
+ isdst: false,
+ text: '(UTC-04:00) Asuncion',
+ utc: ['America/Asuncion'],
+ },
+ {
+ value: 'Atlantic Standard Time',
+ abbr: 'ADT',
+ offset: -3,
+ isdst: true,
+ text: '(UTC-04:00) Atlantic Time (Canada)',
+ utc: [
+ 'America/Glace_Bay',
+ 'America/Goose_Bay',
+ 'America/Halifax',
+ 'America/Moncton',
+ 'America/Thule',
+ 'Atlantic/Bermuda',
+ ],
+ },
+ {
+ value: 'Central Brazilian Standard Time',
+ abbr: 'CBST',
+ offset: -4,
+ isdst: false,
+ text: '(UTC-04:00) Cuiaba',
+ utc: ['America/Campo_Grande', 'America/Cuiaba'],
+ },
+ {
+ value: 'SA Western Standard Time',
+ abbr: 'SWST',
+ offset: -4,
+ isdst: false,
+ text: '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan',
+ utc: [
+ 'America/Anguilla',
+ 'America/Antigua',
+ 'America/Aruba',
+ 'America/Barbados',
+ 'America/Blanc-Sablon',
+ 'America/Boa_Vista',
+ 'America/Curacao',
+ 'America/Dominica',
+ 'America/Grand_Turk',
+ 'America/Grenada',
+ 'America/Guadeloupe',
+ 'America/Guyana',
+ 'America/Kralendijk',
+ 'America/La_Paz',
+ 'America/Lower_Princes',
+ 'America/Manaus',
+ 'America/Marigot',
+ 'America/Martinique',
+ 'America/Montserrat',
+ 'America/Port_of_Spain',
+ 'America/Porto_Velho',
+ 'America/Puerto_Rico',
+ 'America/Santo_Domingo',
+ 'America/St_Barthelemy',
+ 'America/St_Kitts',
+ 'America/St_Lucia',
+ 'America/St_Thomas',
+ 'America/St_Vincent',
+ 'America/Tortola',
+ 'Etc/GMT+4',
+ ],
+ },
+ {
+ value: 'Pacific SA Standard Time',
+ abbr: 'PSST',
+ offset: -4,
+ isdst: false,
+ text: '(UTC-04:00) Santiago',
+ utc: ['America/Santiago', 'Antarctica/Palmer'],
+ },
+ {
+ value: 'Newfoundland Standard Time',
+ abbr: 'NDT',
+ offset: -2.5,
+ isdst: true,
+ text: '(UTC-03:30) Newfoundland',
+ utc: ['America/St_Johns'],
+ },
+ {
+ value: 'E. South America Standard Time',
+ abbr: 'ESAST',
+ offset: -3,
+ isdst: false,
+ text: '(UTC-03:00) Brasilia',
+ utc: ['America/Sao_Paulo'],
+ },
+ {
+ value: 'Argentina Standard Time',
+ abbr: 'AST',
+ offset: -3,
+ isdst: false,
+ text: '(UTC-03:00) Buenos Aires',
+ utc: [
+ 'America/Argentina/La_Rioja',
+ 'America/Argentina/Rio_Gallegos',
+ 'America/Argentina/Salta',
+ 'America/Argentina/San_Juan',
+ 'America/Argentina/San_Luis',
+ 'America/Argentina/Tucuman',
+ 'America/Argentina/Ushuaia',
+ 'America/Buenos_Aires',
+ 'America/Catamarca',
+ 'America/Cordoba',
+ 'America/Jujuy',
+ 'America/Mendoza',
+ ],
+ },
+ {
+ value: 'SA Eastern Standard Time',
+ abbr: 'SEST',
+ offset: -3,
+ isdst: false,
+ text: '(UTC-03:00) Cayenne, Fortaleza',
+ utc: [
+ 'America/Araguaina',
+ 'America/Belem',
+ 'America/Cayenne',
+ 'America/Fortaleza',
+ 'America/Maceio',
+ 'America/Paramaribo',
+ 'America/Recife',
+ 'America/Santarem',
+ 'Antarctica/Rothera',
+ 'Atlantic/Stanley',
+ 'Etc/GMT+3',
+ ],
+ },
+ {
+ value: 'Greenland Standard Time',
+ abbr: 'GDT',
+ offset: -3,
+ isdst: true,
+ text: '(UTC-03:00) Greenland',
+ utc: ['America/Godthab'],
+ },
+ {
+ value: 'Montevideo Standard Time',
+ abbr: 'MST',
+ offset: -3,
+ isdst: false,
+ text: '(UTC-03:00) Montevideo',
+ utc: ['America/Montevideo'],
+ },
+ {
+ value: 'Bahia Standard Time',
+ abbr: 'BST',
+ offset: -3,
+ isdst: false,
+ text: '(UTC-03:00) Salvador',
+ utc: ['America/Bahia'],
+ },
+ {
+ value: 'UTC-02',
+ abbr: 'U',
+ offset: -2,
+ isdst: false,
+ text: '(UTC-02:00) Coordinated Universal Time-02',
+ utc: ['America/Noronha', 'Atlantic/South_Georgia', 'Etc/GMT+2'],
+ },
+ {
+ value: 'Azores Standard Time',
+ abbr: 'ADT',
+ offset: 0,
+ isdst: true,
+ text: '(UTC-01:00) Azores',
+ utc: ['America/Scoresbysund', 'Atlantic/Azores'],
+ },
+ {
+ value: 'Cape Verde Standard Time',
+ abbr: 'CVST',
+ offset: -1,
+ isdst: false,
+ text: '(UTC-01:00) Cape Verde Is.',
+ utc: ['Atlantic/Cape_Verde', 'Etc/GMT+1'],
+ },
+ {
+ value: 'Morocco Standard Time',
+ abbr: 'MDT',
+ offset: 1,
+ isdst: true,
+ text: '(UTC) Casablanca',
+ utc: ['Africa/Casablanca', 'Africa/El_Aaiun'],
+ },
+ {
+ value: 'UTC',
+ abbr: 'UTC',
+ offset: 0,
+ isdst: false,
+ text: '(UTC) Coordinated Universal Time',
+ utc: ['America/Danmarkshavn', 'Etc/GMT'],
+ },
+ {
+ value: 'British Summer Time',
+ abbr: 'BST',
+ offset: 1,
+ isdst: true,
+ text: '(UTC+01:00) Edinburgh, London',
+ utc: ['Europe/Isle_of_Man', 'Europe/Guernsey', 'Europe/Jersey', 'Europe/London'],
+ },
+ {
+ value: 'GMT Standard Time',
+ abbr: 'GDT',
+ offset: 1,
+ isdst: true,
+ text: '(UTC) Dublin, Lisbon',
+ utc: [
+ 'Atlantic/Canary',
+ 'Atlantic/Faeroe',
+ 'Atlantic/Madeira',
+ 'Europe/Dublin',
+ 'Europe/Lisbon',
+ ],
+ },
+ {
+ value: 'Greenwich Standard Time',
+ abbr: 'GST',
+ offset: 0,
+ isdst: false,
+ text: '(UTC) Monrovia, Reykjavik',
+ utc: [
+ 'Africa/Abidjan',
+ 'Africa/Accra',
+ 'Africa/Bamako',
+ 'Africa/Banjul',
+ 'Africa/Bissau',
+ 'Africa/Conakry',
+ 'Africa/Dakar',
+ 'Africa/Freetown',
+ 'Africa/Lome',
+ 'Africa/Monrovia',
+ 'Africa/Nouakchott',
+ 'Africa/Ouagadougou',
+ 'Africa/Sao_Tome',
+ 'Atlantic/Reykjavik',
+ 'Atlantic/St_Helena',
+ ],
+ },
+ {
+ value: 'W. Europe Standard Time',
+ abbr: 'WEDT',
+ offset: 2,
+ isdst: true,
+ text: '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna',
+ utc: [
+ 'Arctic/Longyearbyen',
+ 'Europe/Amsterdam',
+ 'Europe/Andorra',
+ 'Europe/Berlin',
+ 'Europe/Busingen',
+ 'Europe/Gibraltar',
+ 'Europe/Luxembourg',
+ 'Europe/Malta',
+ 'Europe/Monaco',
+ 'Europe/Oslo',
+ 'Europe/Rome',
+ 'Europe/San_Marino',
+ 'Europe/Stockholm',
+ 'Europe/Vaduz',
+ 'Europe/Vatican',
+ 'Europe/Vienna',
+ 'Europe/Zurich',
+ ],
+ },
+ {
+ value: 'Central Europe Standard Time',
+ abbr: 'CEDT',
+ offset: 2,
+ isdst: true,
+ text: '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague',
+ utc: [
+ 'Europe/Belgrade',
+ 'Europe/Bratislava',
+ 'Europe/Budapest',
+ 'Europe/Ljubljana',
+ 'Europe/Podgorica',
+ 'Europe/Prague',
+ 'Europe/Tirane',
+ ],
+ },
+ {
+ value: 'Romance Standard Time',
+ abbr: 'RDT',
+ offset: 2,
+ isdst: true,
+ text: '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris',
+ utc: ['Africa/Ceuta', 'Europe/Brussels', 'Europe/Copenhagen', 'Europe/Madrid', 'Europe/Paris'],
+ },
+ {
+ value: 'Central European Standard Time',
+ abbr: 'CEDT',
+ offset: 2,
+ isdst: true,
+ text: '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb',
+ utc: ['Europe/Sarajevo', 'Europe/Skopje', 'Europe/Warsaw', 'Europe/Zagreb'],
+ },
+ {
+ value: 'W. Central Africa Standard Time',
+ abbr: 'WCAST',
+ offset: 1,
+ isdst: false,
+ text: '(UTC+01:00) West Central Africa',
+ utc: [
+ 'Africa/Algiers',
+ 'Africa/Bangui',
+ 'Africa/Brazzaville',
+ 'Africa/Douala',
+ 'Africa/Kinshasa',
+ 'Africa/Lagos',
+ 'Africa/Libreville',
+ 'Africa/Luanda',
+ 'Africa/Malabo',
+ 'Africa/Ndjamena',
+ 'Africa/Niamey',
+ 'Africa/Porto-Novo',
+ 'Africa/Tunis',
+ 'Etc/GMT-1',
+ ],
+ },
+ {
+ value: 'Namibia Standard Time',
+ abbr: 'NST',
+ offset: 1,
+ isdst: false,
+ text: '(UTC+01:00) Windhoek',
+ utc: ['Africa/Windhoek'],
+ },
+ {
+ value: 'GTB Standard Time',
+ abbr: 'GDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) Athens, Bucharest',
+ utc: ['Asia/Nicosia', 'Europe/Athens', 'Europe/Bucharest', 'Europe/Chisinau'],
+ },
+ {
+ value: 'Middle East Standard Time',
+ abbr: 'MEDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) Beirut',
+ utc: ['Asia/Beirut'],
+ },
+ {
+ value: 'Egypt Standard Time',
+ abbr: 'EST',
+ offset: 2,
+ isdst: false,
+ text: '(UTC+02:00) Cairo',
+ utc: ['Africa/Cairo'],
+ },
+ {
+ value: 'Syria Standard Time',
+ abbr: 'SDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) Damascus',
+ utc: ['Asia/Damascus'],
+ },
+ {
+ value: 'E. Europe Standard Time',
+ abbr: 'EEDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) E. Europe',
+ utc: [
+ 'Asia/Nicosia',
+ 'Europe/Athens',
+ 'Europe/Bucharest',
+ 'Europe/Chisinau',
+ 'Europe/Helsinki',
+ 'Europe/Kiev',
+ 'Europe/Mariehamn',
+ 'Europe/Nicosia',
+ 'Europe/Riga',
+ 'Europe/Sofia',
+ 'Europe/Tallinn',
+ 'Europe/Uzhgorod',
+ 'Europe/Vilnius',
+ 'Europe/Zaporozhye',
+ ],
+ },
+ {
+ value: 'South Africa Standard Time',
+ abbr: 'SAST',
+ offset: 2,
+ isdst: false,
+ text: '(UTC+02:00) Harare, Pretoria',
+ utc: [
+ 'Africa/Blantyre',
+ 'Africa/Bujumbura',
+ 'Africa/Gaborone',
+ 'Africa/Harare',
+ 'Africa/Johannesburg',
+ 'Africa/Kigali',
+ 'Africa/Lubumbashi',
+ 'Africa/Lusaka',
+ 'Africa/Maputo',
+ 'Africa/Maseru',
+ 'Africa/Mbabane',
+ 'Etc/GMT-2',
+ ],
+ },
+ {
+ value: 'FLE Standard Time',
+ abbr: 'FDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius',
+ utc: [
+ 'Europe/Helsinki',
+ 'Europe/Kiev',
+ 'Europe/Mariehamn',
+ 'Europe/Riga',
+ 'Europe/Sofia',
+ 'Europe/Tallinn',
+ 'Europe/Uzhgorod',
+ 'Europe/Vilnius',
+ 'Europe/Zaporozhye',
+ ],
+ },
+ {
+ value: 'Turkey Standard Time',
+ abbr: 'TDT',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Istanbul',
+ utc: ['Europe/Istanbul'],
+ },
+ {
+ value: 'Israel Standard Time',
+ abbr: 'JDT',
+ offset: 3,
+ isdst: true,
+ text: '(UTC+02:00) Jerusalem',
+ utc: ['Asia/Jerusalem'],
+ },
+ {
+ value: 'Libya Standard Time',
+ abbr: 'LST',
+ offset: 2,
+ isdst: false,
+ text: '(UTC+02:00) Tripoli',
+ utc: ['Africa/Tripoli'],
+ },
+ {
+ value: 'Jordan Standard Time',
+ abbr: 'JST',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Amman',
+ utc: ['Asia/Amman'],
+ },
+ {
+ value: 'Arabic Standard Time',
+ abbr: 'AST',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Baghdad',
+ utc: ['Asia/Baghdad'],
+ },
+ {
+ value: 'Kaliningrad Standard Time',
+ abbr: 'KST',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+02:00) Kaliningrad',
+ utc: ['Europe/Kaliningrad'],
+ },
+ {
+ value: 'Arab Standard Time',
+ abbr: 'AST',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Kuwait, Riyadh',
+ utc: ['Asia/Aden', 'Asia/Bahrain', 'Asia/Kuwait', 'Asia/Qatar', 'Asia/Riyadh'],
+ },
+ {
+ value: 'E. Africa Standard Time',
+ abbr: 'EAST',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Nairobi',
+ utc: [
+ 'Africa/Addis_Ababa',
+ 'Africa/Asmera',
+ 'Africa/Dar_es_Salaam',
+ 'Africa/Djibouti',
+ 'Africa/Juba',
+ 'Africa/Kampala',
+ 'Africa/Khartoum',
+ 'Africa/Mogadishu',
+ 'Africa/Nairobi',
+ 'Antarctica/Syowa',
+ 'Etc/GMT-3',
+ 'Indian/Antananarivo',
+ 'Indian/Comoro',
+ 'Indian/Mayotte',
+ ],
+ },
+ {
+ value: 'Moscow Standard Time',
+ abbr: 'MSK',
+ offset: 3,
+ isdst: false,
+ text: '(UTC+03:00) Moscow, St. Petersburg, Volgograd, Minsk',
+ utc: ['Europe/Kirov', 'Europe/Moscow', 'Europe/Simferopol', 'Europe/Volgograd', 'Europe/Minsk'],
+ },
+ {
+ value: 'Samara Time',
+ abbr: 'SAMT',
+ offset: 4,
+ isdst: false,
+ text: '(UTC+04:00) Samara, Ulyanovsk, Saratov',
+ utc: ['Europe/Astrakhan', 'Europe/Samara', 'Europe/Ulyanovsk'],
+ },
+ {
+ value: 'Iran Standard Time',
+ abbr: 'IDT',
+ offset: 4.5,
+ isdst: true,
+ text: '(UTC+03:30) Tehran',
+ utc: ['Asia/Tehran'],
+ },
+ {
+ value: 'Arabian Standard Time',
+ abbr: 'AST',
+ offset: 4,
+ isdst: false,
+ text: '(UTC+04:00) Abu Dhabi, Muscat',
+ utc: ['Asia/Dubai', 'Asia/Muscat', 'Etc/GMT-4'],
+ },
+ {
+ value: 'Azerbaijan Standard Time',
+ abbr: 'ADT',
+ offset: 5,
+ isdst: true,
+ text: '(UTC+04:00) Baku',
+ utc: ['Asia/Baku'],
+ },
+ {
+ value: 'Mauritius Standard Time',
+ abbr: 'MST',
+ offset: 4,
+ isdst: false,
+ text: '(UTC+04:00) Port Louis',
+ utc: ['Indian/Mahe', 'Indian/Mauritius', 'Indian/Reunion'],
+ },
+ {
+ value: 'Georgian Standard Time',
+ abbr: 'GET',
+ offset: 4,
+ isdst: false,
+ text: '(UTC+04:00) Tbilisi',
+ utc: ['Asia/Tbilisi'],
+ },
+ {
+ value: 'Caucasus Standard Time',
+ abbr: 'CST',
+ offset: 4,
+ isdst: false,
+ text: '(UTC+04:00) Yerevan',
+ utc: ['Asia/Yerevan'],
+ },
+ {
+ value: 'Afghanistan Standard Time',
+ abbr: 'AST',
+ offset: 4.5,
+ isdst: false,
+ text: '(UTC+04:30) Kabul',
+ utc: ['Asia/Kabul'],
+ },
+ {
+ value: 'West Asia Standard Time',
+ abbr: 'WAST',
+ offset: 5,
+ isdst: false,
+ text: '(UTC+05:00) Ashgabat, Tashkent',
+ utc: [
+ 'Antarctica/Mawson',
+ 'Asia/Aqtau',
+ 'Asia/Aqtobe',
+ 'Asia/Ashgabat',
+ 'Asia/Dushanbe',
+ 'Asia/Oral',
+ 'Asia/Samarkand',
+ 'Asia/Tashkent',
+ 'Etc/GMT-5',
+ 'Indian/Kerguelen',
+ 'Indian/Maldives',
+ ],
+ },
+ {
+ value: 'Yekaterinburg Time',
+ abbr: 'YEKT',
+ offset: 5,
+ isdst: false,
+ text: '(UTC+05:00) Yekaterinburg',
+ utc: ['Asia/Yekaterinburg'],
+ },
+ {
+ value: 'Pakistan Standard Time',
+ abbr: 'PKT',
+ offset: 5,
+ isdst: false,
+ text: '(UTC+05:00) Islamabad, Karachi',
+ utc: ['Asia/Karachi'],
+ },
+ {
+ value: 'India Standard Time',
+ abbr: 'IST',
+ offset: 5.5,
+ isdst: false,
+ text: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi',
+ utc: ['Asia/Kolkata', 'Asia/Calcutta'],
+ },
+ {
+ value: 'Sri Lanka Standard Time',
+ abbr: 'SLST',
+ offset: 5.5,
+ isdst: false,
+ text: '(UTC+05:30) Sri Jayawardenepura',
+ utc: ['Asia/Colombo'],
+ },
+ {
+ value: 'Nepal Standard Time',
+ abbr: 'NST',
+ offset: 5.75,
+ isdst: false,
+ text: '(UTC+05:45) Kathmandu',
+ utc: ['Asia/Kathmandu'],
+ },
+ {
+ value: 'Central Asia Standard Time',
+ abbr: 'CAST',
+ offset: 6,
+ isdst: false,
+ text: '(UTC+06:00) Nur-Sultan (Astana)',
+ utc: [
+ 'Antarctica/Vostok',
+ 'Asia/Almaty',
+ 'Asia/Bishkek',
+ 'Asia/Qyzylorda',
+ 'Asia/Urumqi',
+ 'Etc/GMT-6',
+ 'Indian/Chagos',
+ ],
+ },
+ {
+ value: 'Bangladesh Standard Time',
+ abbr: 'BST',
+ offset: 6,
+ isdst: false,
+ text: '(UTC+06:00) Dhaka',
+ utc: ['Asia/Dhaka', 'Asia/Thimphu'],
+ },
+ {
+ value: 'Myanmar Standard Time',
+ abbr: 'MST',
+ offset: 6.5,
+ isdst: false,
+ text: '(UTC+06:30) Yangon (Rangoon)',
+ utc: ['Asia/Rangoon', 'Indian/Cocos'],
+ },
+ {
+ value: 'SE Asia Standard Time',
+ abbr: 'SAST',
+ offset: 7,
+ isdst: false,
+ text: '(UTC+07:00) Bangkok, Hanoi, Jakarta',
+ utc: [
+ 'Antarctica/Davis',
+ 'Asia/Bangkok',
+ 'Asia/Hovd',
+ 'Asia/Jakarta',
+ 'Asia/Phnom_Penh',
+ 'Asia/Pontianak',
+ 'Asia/Saigon',
+ 'Asia/Vientiane',
+ 'Etc/GMT-7',
+ 'Indian/Christmas',
+ ],
+ },
+ {
+ value: 'N. Central Asia Standard Time',
+ abbr: 'NCAST',
+ offset: 7,
+ isdst: false,
+ text: '(UTC+07:00) Novosibirsk',
+ utc: ['Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk'],
+ },
+ {
+ value: 'China Standard Time',
+ abbr: 'CST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi',
+ utc: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'],
+ },
+ {
+ value: 'North Asia Standard Time',
+ abbr: 'NAST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Krasnoyarsk',
+ utc: ['Asia/Krasnoyarsk'],
+ },
+ {
+ value: 'Singapore Standard Time',
+ abbr: 'MPST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Kuala Lumpur, Singapore',
+ utc: [
+ 'Asia/Brunei',
+ 'Asia/Kuala_Lumpur',
+ 'Asia/Kuching',
+ 'Asia/Makassar',
+ 'Asia/Manila',
+ 'Asia/Singapore',
+ 'Etc/GMT-8',
+ ],
+ },
+ {
+ value: 'W. Australia Standard Time',
+ abbr: 'WAST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Perth',
+ utc: ['Antarctica/Casey', 'Australia/Perth'],
+ },
+ {
+ value: 'Taipei Standard Time',
+ abbr: 'TST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Taipei',
+ utc: ['Asia/Taipei'],
+ },
+ {
+ value: 'Ulaanbaatar Standard Time',
+ abbr: 'UST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Ulaanbaatar',
+ utc: ['Asia/Choibalsan', 'Asia/Ulaanbaatar'],
+ },
+ {
+ value: 'North Asia East Standard Time',
+ abbr: 'NAEST',
+ offset: 8,
+ isdst: false,
+ text: '(UTC+08:00) Irkutsk',
+ utc: ['Asia/Irkutsk'],
+ },
+ {
+ value: 'Japan Standard Time',
+ abbr: 'JST',
+ offset: 9,
+ isdst: false,
+ text: '(UTC+09:00) Osaka, Sapporo, Tokyo',
+ utc: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau'],
+ },
+ {
+ value: 'Korea Standard Time',
+ abbr: 'KST',
+ offset: 9,
+ isdst: false,
+ text: '(UTC+09:00) Seoul',
+ utc: ['Asia/Pyongyang', 'Asia/Seoul'],
+ },
+ {
+ value: 'Cen. Australia Standard Time',
+ abbr: 'CAST',
+ offset: 9.5,
+ isdst: false,
+ text: '(UTC+09:30) Adelaide',
+ utc: ['Australia/Adelaide', 'Australia/Broken_Hill'],
+ },
+ {
+ value: 'AUS Central Standard Time',
+ abbr: 'ACST',
+ offset: 9.5,
+ isdst: false,
+ text: '(UTC+09:30) Darwin',
+ utc: ['Australia/Darwin'],
+ },
+ {
+ value: 'E. Australia Standard Time',
+ abbr: 'EAST',
+ offset: 10,
+ isdst: false,
+ text: '(UTC+10:00) Brisbane',
+ utc: ['Australia/Brisbane', 'Australia/Lindeman'],
+ },
+ {
+ value: 'AUS Eastern Standard Time',
+ abbr: 'AEST',
+ offset: 10,
+ isdst: false,
+ text: '(UTC+10:00) Canberra, Melbourne, Sydney',
+ utc: ['Australia/Melbourne', 'Australia/Sydney'],
+ },
+ {
+ value: 'West Pacific Standard Time',
+ abbr: 'WPST',
+ offset: 10,
+ isdst: false,
+ text: '(UTC+10:00) Guam, Port Moresby',
+ utc: [
+ 'Antarctica/DumontDUrville',
+ 'Etc/GMT-10',
+ 'Pacific/Guam',
+ 'Pacific/Port_Moresby',
+ 'Pacific/Saipan',
+ 'Pacific/Truk',
+ ],
+ },
+ {
+ value: 'Tasmania Standard Time',
+ abbr: 'TST',
+ offset: 10,
+ isdst: false,
+ text: '(UTC+10:00) Hobart',
+ utc: ['Australia/Currie', 'Australia/Hobart'],
+ },
+ {
+ value: 'Yakutsk Standard Time',
+ abbr: 'YST',
+ offset: 9,
+ isdst: false,
+ text: '(UTC+09:00) Yakutsk',
+ utc: ['Asia/Chita', 'Asia/Khandyga', 'Asia/Yakutsk'],
+ },
+ {
+ value: 'Central Pacific Standard Time',
+ abbr: 'CPST',
+ offset: 11,
+ isdst: false,
+ text: '(UTC+11:00) Solomon Is., New Caledonia',
+ utc: [
+ 'Antarctica/Macquarie',
+ 'Etc/GMT-11',
+ 'Pacific/Efate',
+ 'Pacific/Guadalcanal',
+ 'Pacific/Kosrae',
+ 'Pacific/Noumea',
+ 'Pacific/Ponape',
+ ],
+ },
+ {
+ value: 'Vladivostok Standard Time',
+ abbr: 'VST',
+ offset: 11,
+ isdst: false,
+ text: '(UTC+11:00) Vladivostok',
+ utc: ['Asia/Sakhalin', 'Asia/Ust-Nera', 'Asia/Vladivostok'],
+ },
+ {
+ value: 'New Zealand Standard Time',
+ abbr: 'NZST',
+ offset: 12,
+ isdst: false,
+ text: '(UTC+12:00) Auckland, Wellington',
+ utc: ['Antarctica/McMurdo', 'Pacific/Auckland'],
+ },
+ {
+ value: 'UTC+12',
+ abbr: 'U',
+ offset: 12,
+ isdst: false,
+ text: '(UTC+12:00) Coordinated Universal Time+12',
+ utc: [
+ 'Etc/GMT-12',
+ 'Pacific/Funafuti',
+ 'Pacific/Kwajalein',
+ 'Pacific/Majuro',
+ 'Pacific/Nauru',
+ 'Pacific/Tarawa',
+ 'Pacific/Wake',
+ 'Pacific/Wallis',
+ ],
+ },
+ {
+ value: 'Fiji Standard Time',
+ abbr: 'FST',
+ offset: 12,
+ isdst: false,
+ text: '(UTC+12:00) Fiji',
+ utc: ['Pacific/Fiji'],
+ },
+ {
+ value: 'Magadan Standard Time',
+ abbr: 'MST',
+ offset: 12,
+ isdst: false,
+ text: '(UTC+12:00) Magadan',
+ utc: ['Asia/Anadyr', 'Asia/Kamchatka', 'Asia/Magadan', 'Asia/Srednekolymsk'],
+ },
+ {
+ value: 'Kamchatka Standard Time',
+ abbr: 'KDT',
+ offset: 13,
+ isdst: true,
+ text: '(UTC+12:00) Petropavlovsk-Kamchatsky - Old',
+ utc: ['Asia/Kamchatka'],
+ },
+ {
+ value: 'Tonga Standard Time',
+ abbr: 'TST',
+ offset: 13,
+ isdst: false,
+ text: "(UTC+13:00) Nuku'alofa",
+ utc: ['Etc/GMT-13', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Tongatapu'],
+ },
+ {
+ value: 'Samoa Standard Time',
+ abbr: 'SST',
+ offset: 13,
+ isdst: false,
+ text: '(UTC+13:00) Samoa',
+ utc: ['Pacific/Apia'],
+ },
+]
+
+/**
+ * Deduplicated view of `ALL_TIMEZONES`, keyed by the entry's primary IANA
+ * name (`utc[0]`). Several catalog rows share an IANA target — e.g. PDT and
+ * PST both point to `America/Los_Angeles` — which would render duplicate UI
+ * rows that all store the same value when picked. We prefer the standard-time
+ * (`isdst: false`) row when both are present.
+ */
+export const TIMEZONES_BY_IANA: Timezone[] = (() => {
+ const seen = new Map()
+ for (const entry of ALL_TIMEZONES) {
+ const key = entry.utc[0]
+ if (!key) continue
+ const existing = seen.get(key)
+ if (!existing || (existing.isdst && !entry.isdst)) {
+ seen.set(key, entry)
+ }
+ }
+ return Array.from(seen.values())
+})()
+
+/** Look up the catalog entry that owns the given IANA name (any of `utc[]`). */
+export const findTimezoneByIana = (iana: string): Timezone | undefined =>
+ ALL_TIMEZONES.find((entry) => entry.utc.includes(iana))
diff --git a/apps/studio/lib/datetime.test.ts b/apps/studio/lib/datetime.test.ts
new file mode 100644
index 00000000000..aea878c0fd9
--- /dev/null
+++ b/apps/studio/lib/datetime.test.ts
@@ -0,0 +1,122 @@
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
+
+import {
+ formatDate,
+ formatDateTime,
+ formatFromNow,
+ formatTime,
+ resolveTimezone,
+ toTimezone,
+} from '@/lib/datetime'
+
+// Fixed reference points so the assertions don't depend on the host machine's
+// timezone or current wall-clock time.
+const SUMMER_UTC = '2025-06-15T12:34:56Z'
+const WINTER_UTC = '2025-01-15T12:34:56Z'
+// 16-digit unix microseconds equivalent of SUMMER_UTC.
+const SUMMER_UNIX_MICRO = '1749990896000000'
+
+describe('formatDateTime', () => {
+ it('renders a UTC instant in Asia/Tokyo (UTC+9, no DST)', () => {
+ expect(formatDateTime(SUMMER_UTC, { tz: 'Asia/Tokyo' })).toBe('15 Jun 2025 21:34:56')
+ })
+
+ it('renders a UTC instant in America/Los_Angeles during DST (UTC-7)', () => {
+ expect(formatDateTime(SUMMER_UTC, { tz: 'America/Los_Angeles' })).toBe('15 Jun 2025 05:34:56')
+ })
+
+ it('renders a UTC instant in America/Los_Angeles outside DST (UTC-8)', () => {
+ expect(formatDateTime(WINTER_UTC, { tz: 'America/Los_Angeles' })).toBe('15 Jan 2025 04:34:56')
+ })
+
+ it('flips the wall-clock day when crossing date boundaries', () => {
+ const lateUtc = '2025-06-15T22:00:00Z'
+ expect(formatDateTime(lateUtc, { tz: 'Asia/Tokyo', format: 'YYYY-MM-DD' })).toBe('2025-06-16')
+ expect(formatDateTime(lateUtc, { tz: 'Pacific/Honolulu', format: 'YYYY-MM-DD' })).toBe(
+ '2025-06-15'
+ )
+ })
+
+ it('respects an explicit format string', () => {
+ expect(formatDateTime(SUMMER_UTC, { tz: 'UTC', format: 'YYYY-MM-DDTHH:mm:ssZ' })).toBe(
+ '2025-06-15T12:34:56+00:00'
+ )
+ })
+
+ it('accepts unix microsecond timestamps', () => {
+ expect(formatDateTime(SUMMER_UNIX_MICRO, { tz: 'UTC' })).toBe('15 Jun 2025 12:34:56')
+ })
+
+ it('accepts Date instances', () => {
+ expect(formatDateTime(new Date(SUMMER_UTC), { tz: 'UTC' })).toBe('15 Jun 2025 12:34:56')
+ })
+})
+
+describe('DST transitions in Europe/Berlin', () => {
+ it('shows +01:00 before the spring transition', () => {
+ expect(formatDateTime('2025-03-30T00:00:00Z', { tz: 'Europe/Berlin', format: 'HH:mm Z' })).toBe(
+ '01:00 +01:00'
+ )
+ })
+
+ it('shows +02:00 after the spring transition', () => {
+ expect(formatDateTime('2025-03-30T02:00:00Z', { tz: 'Europe/Berlin', format: 'HH:mm Z' })).toBe(
+ '04:00 +02:00'
+ )
+ })
+})
+
+describe('formatDate / formatTime', () => {
+ it('formatDate uses the date-only default', () => {
+ expect(formatDate(SUMMER_UTC, { tz: 'UTC' })).toBe('15 Jun 2025')
+ })
+
+ it('formatTime uses the time-only default', () => {
+ expect(formatTime(SUMMER_UTC, { tz: 'UTC' })).toBe('12:34:56')
+ })
+})
+
+describe('resolveTimezone', () => {
+ it('returns the input when it is a valid IANA name', () => {
+ expect(resolveTimezone('Asia/Tokyo')).toBe('Asia/Tokyo')
+ })
+
+ it('falls back to the guessed local timezone when input is empty', () => {
+ expect(resolveTimezone('')).toBeTruthy()
+ expect(resolveTimezone(null)).toBeTruthy()
+ expect(resolveTimezone(undefined)).toBeTruthy()
+ })
+
+ it('falls back when the input is not a real IANA zone', () => {
+ // Should not throw, should resolve to something we can format with.
+ const tz = resolveTimezone('Not/A/Real_Zone')
+ expect(() => formatDateTime(SUMMER_UTC, { tz })).not.toThrow()
+ })
+})
+
+describe('toTimezone', () => {
+ it('returns a Dayjs pinned to the given timezone for chained calls', () => {
+ const d = toTimezone(SUMMER_UTC, 'Asia/Tokyo')
+ expect(d.format('HH:mm')).toBe('21:34')
+ expect(d.hour()).toBe(21)
+ })
+})
+
+describe('formatFromNow', () => {
+ beforeAll(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2025-06-15T13:34:56Z'))
+ })
+
+ afterAll(() => {
+ vi.useRealTimers()
+ })
+
+ it('renders ISO inputs as a relative duration', () => {
+ expect(formatFromNow(SUMMER_UTC)).toBe('an hour ago')
+ })
+
+ it('accepts unix microsecond inputs', () => {
+ expect(formatFromNow(SUMMER_UNIX_MICRO)).toBe('an hour ago')
+ })
+})
diff --git a/apps/studio/lib/datetime.tsx b/apps/studio/lib/datetime.tsx
new file mode 100644
index 00000000000..3310ed743cc
--- /dev/null
+++ b/apps/studio/lib/datetime.tsx
@@ -0,0 +1,185 @@
+import { LOCAL_STORAGE_KEYS } from 'common'
+import dayjs, { type Dayjs } from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import timezone from 'dayjs/plugin/timezone'
+import utc from 'dayjs/plugin/utc'
+import { createContext, useCallback, useContext, useEffect, useMemo, type ReactNode } from 'react'
+
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+import { guessLocalTimezone } from '@/lib/dayjs'
+
+// dayjs.extend is idempotent. Extending here removes the implicit dependency
+// on _app.tsx running first (e.g. Storybook, isolated scripts).
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(relativeTime)
+
+export type DateInput = string | number | Date | Dayjs
+
+const isUnixMicro = (value: string | number): boolean => {
+ const digits = String(value).length
+ const isNum = !Number.isNaN(Number(value))
+ return isNum && digits === 16
+}
+
+const unixMicroToIso = (value: string | number): string =>
+ dayjs.unix(Number(value) / 1_000_000).toISOString()
+
+const normalize = (input: DateInput): Dayjs => {
+ if (dayjs.isDayjs(input)) return input
+ if (input instanceof Date) return dayjs(input)
+ if ((typeof input === 'string' || typeof input === 'number') && isUnixMicro(input)) {
+ return dayjs.utc(unixMicroToIso(input))
+ }
+ return dayjs.utc(input)
+}
+
+const isValidTimezone = (tz: string): boolean => {
+ try {
+ Intl.DateTimeFormat(undefined, { timeZone: tz })
+ return true
+ } catch {
+ return false
+ }
+}
+
+/**
+ * Resolve a user-supplied timezone to a valid IANA name. Falls back to the
+ * browser's guessed timezone, then UTC. Pass `undefined`/empty string to opt
+ * into the guessed default.
+ */
+export const resolveTimezone = (tz: string | undefined | null): string => {
+ if (tz && isValidTimezone(tz)) return tz
+ return guessLocalTimezone()
+}
+
+const DEFAULT_DATETIME_FORMAT = 'DD MMM YYYY HH:mm:ss'
+const DEFAULT_DATE_FORMAT = 'DD MMM YYYY'
+const DEFAULT_TIME_FORMAT = 'HH:mm:ss'
+
+interface FormatOptions {
+ /** IANA timezone (e.g. 'Asia/Tokyo'). Falls back to guessed local. */
+ tz?: string
+ /** dayjs format string. */
+ format?: string
+}
+
+export const formatDateTime = (input: DateInput, opts: FormatOptions = {}): string =>
+ normalize(input)
+ .tz(resolveTimezone(opts.tz))
+ .format(opts.format ?? DEFAULT_DATETIME_FORMAT)
+
+export const formatDate = (input: DateInput, opts: FormatOptions = {}): string =>
+ normalize(input)
+ .tz(resolveTimezone(opts.tz))
+ .format(opts.format ?? DEFAULT_DATE_FORMAT)
+
+export const formatTime = (input: DateInput, opts: FormatOptions = {}): string =>
+ normalize(input)
+ .tz(resolveTimezone(opts.tz))
+ .format(opts.format ?? DEFAULT_TIME_FORMAT)
+
+/** Returns a humanised relative time, e.g. "3 minutes ago". */
+export const formatFromNow = (input: DateInput): string => normalize(input).fromNow()
+
+/** Returns the input as a Dayjs instance pinned to the given timezone. */
+export const toTimezone = (input: DateInput, tz?: string): Dayjs =>
+ normalize(input).tz(resolveTimezone(tz))
+
+interface TimezoneContextValue {
+ /** The resolved IANA timezone currently in use. Always valid. */
+ timezone: string
+ /** The user's stored preference. Empty string means "use guessed local". */
+ storedTimezone: string
+ /** Update the stored preference. Pass an empty string to clear (use guessed). */
+ setTimezone: (tz: string) => void
+ /** Whether the current selection is the auto-detected default. */
+ isAutoDetected: boolean
+}
+
+const TimezoneContext = createContext(undefined)
+
+export const TimezoneProvider = ({ children }: { children: ReactNode }) => {
+ const [storedTimezone, setStoredTimezone] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.UI_TIMEZONE,
+ ''
+ )
+
+ const timezone = useMemo(() => resolveTimezone(storedTimezone), [storedTimezone])
+
+ // Apply the selected timezone as the dayjs default so anything calling
+ // `dayjs.tz()` or `.tz()` without an argument picks it up. Bare `dayjs()`
+ // calls are unaffected by design — those continue to render in the host
+ // browser's timezone until they're intentionally migrated to the wrappers
+ // below.
+ useEffect(() => {
+ dayjs.tz.setDefault(timezone)
+ }, [timezone])
+
+ const setTimezone = useCallback(
+ (tz: string) => {
+ setStoredTimezone(tz)
+ },
+ [setStoredTimezone]
+ )
+
+ const value = useMemo(
+ () => ({
+ timezone,
+ storedTimezone,
+ setTimezone,
+ isAutoDetected: !storedTimezone,
+ }),
+ [timezone, storedTimezone, setTimezone]
+ )
+
+ return {children}
+}
+
+// Stable fallback so callers outside the provider (e.g. unit tests, isolated
+// stories) don't get a fresh object identity every render.
+const NO_OP_SET_TIMEZONE = () => {}
+
+export const useTimezone = (): TimezoneContextValue => {
+ const ctx = useContext(TimezoneContext)
+ return useMemo(
+ () =>
+ ctx ?? {
+ timezone: guessLocalTimezone(),
+ storedTimezone: '',
+ setTimezone: NO_OP_SET_TIMEZONE,
+ isAutoDetected: true,
+ },
+ [ctx]
+ )
+}
+
+/** Returns a memoised `(input, format?) => string` bound to the active timezone. */
+export const useFormatDateTime = () => {
+ const { timezone } = useTimezone()
+ return useCallback(
+ (input: DateInput, format?: string) => formatDateTime(input, { tz: timezone, format }),
+ [timezone]
+ )
+}
+
+export const useFormatDate = () => {
+ const { timezone } = useTimezone()
+ return useCallback(
+ (input: DateInput, format?: string) => formatDate(input, { tz: timezone, format }),
+ [timezone]
+ )
+}
+
+export const useFormatTime = () => {
+ const { timezone } = useTimezone()
+ return useCallback(
+ (input: DateInput, format?: string) => formatTime(input, { tz: timezone, format }),
+ [timezone]
+ )
+}
+
+export const useToTimezone = () => {
+ const { timezone } = useTimezone()
+ return useCallback((input: DateInput) => toTimezone(input, timezone), [timezone])
+}
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index d352a24054f..c764c35b254 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -39,6 +39,7 @@ import { NuqsAdapter } from 'nuqs/adapters/next/pages'
import { ErrorInfo, useCallback, type ComponentProps } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { TooltipProvider } from 'ui'
+import { TimestampInfoProvider } from 'ui-patterns'
import { StudioCommandMenu } from '@/components/interfaces/App/CommandMenu'
import { StudioCommandProvider as CommandProvider } from '@/components/interfaces/App/CommandMenu/StudioCommandProvider'
@@ -57,6 +58,7 @@ import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { AuthProvider } from '@/lib/auth'
import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from '@/lib/constants'
+import { TimezoneProvider, useTimezone } from '@/lib/datetime'
import { ProfileProvider } from '@/lib/profile'
import { Telemetry } from '@/lib/telemetry'
import { Toaster } from '@/lib/toaster'
@@ -96,6 +98,11 @@ const FeatureFlagProviderWithOrgContext = ({
)
}
+const TimestampInfoTimezoneBridge = ({ children }: { children: React.ReactNode }) => {
+ const { timezone } = useTimezone()
+ return {children}
+}
+
loader.config({
// [Joshen] Attempt for offline support/bypass ISP issues is to store the assets required for monaco
// locally. We're however, only storing the assets which we need (based on what the network tab loads
@@ -159,58 +166,62 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
getConfigCatFlags={getConfigCatFlags}
>
-
- {appTitle ?? 'Supabase'}
-
-
-
- {/* [Alaister]: This has to be an inline style tag here and not a separate component due to next/font */}
-
- {/* Speed up initial API loading times by pre-connecting to the API domain */}
- {IS_PLATFORM && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {getLayout( )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!isTestEnv && (
-
- )}
+
+
+
+ {appTitle ?? 'Supabase'}
+
+
+
+ {/* [Alaister]: This has to be an inline style tag here and not a separate component due to next/font */}
+
+ {/* Speed up initial API loading times by pre-connecting to the API domain */}
+ {IS_PLATFORM && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {getLayout( )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isTestEnv && (
+
+ )}
+
+
diff --git a/apps/studio/pages/account/me.tsx b/apps/studio/pages/account/me.tsx
index 9dbe1bbf222..f069bdee82c 100644
--- a/apps/studio/pages/account/me.tsx
+++ b/apps/studio/pages/account/me.tsx
@@ -25,6 +25,7 @@ import { DashboardSettings } from '@/components/interfaces/Account/Preferences/D
import { HotkeySettings } from '@/components/interfaces/Account/Preferences/HotkeySettings'
import { ProfileInformation } from '@/components/interfaces/Account/Preferences/ProfileInformation'
import { ThemeSettings } from '@/components/interfaces/Account/Preferences/ThemeSettings'
+import { TimezoneSettings } from '@/components/interfaces/Account/Preferences/TimezoneSettings'
import AccountLayout from '@/components/layouts/AccountLayout/AccountLayout'
import { AppLayout } from '@/components/layouts/AppLayout/AppLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
@@ -95,6 +96,8 @@ const PlatformPreferences = () => {
+
+
@@ -191,6 +194,8 @@ const SelfHostedPreferences = () => {
+
+
diff --git a/apps/studio/tests/pages/account/me.test.tsx b/apps/studio/tests/pages/account/me.test.tsx
index 7155febbe17..4d372e73db1 100644
--- a/apps/studio/tests/pages/account/me.test.tsx
+++ b/apps/studio/tests/pages/account/me.test.tsx
@@ -57,6 +57,10 @@ vi.mock('@/components/interfaces/Account/Preferences/ThemeSettings', () => ({
ThemeSettings: () => ThemeSettings
,
}))
+vi.mock('@/components/interfaces/Account/Preferences/TimezoneSettings', () => ({
+ TimezoneSettings: () => TimezoneSettings
,
+}))
+
vi.mock('@/components/interfaces/Account/Preferences/HotkeySettings', () => ({
HotkeySettings: () => HotkeySettings
,
}))
@@ -103,6 +107,7 @@ describe('/account/me', () => {
expect(screen.getByText('AccountIdentities')).toBeInTheDocument()
expect(screen.getByText('AccountConnections')).toBeInTheDocument()
expect(screen.getByText('ThemeSettings')).toBeInTheDocument()
+ expect(screen.getByText('TimezoneSettings')).toBeInTheDocument()
expect(screen.getByText('HotkeySettings')).toBeInTheDocument()
expect(screen.getByText('DashboardSettings')).toBeInTheDocument()
expect(screen.getByText('AnalyticsSettings')).toBeInTheDocument()
@@ -115,6 +120,7 @@ describe('/account/me', () => {
render( )
expect(screen.getByText('ThemeSettings')).toBeInTheDocument()
+ expect(screen.getByText('TimezoneSettings')).toBeInTheDocument()
expect(screen.getByText('HotkeySettings')).toBeInTheDocument()
expect(screen.getByText('DashboardSettings')).toBeInTheDocument()
expect(screen.queryByText('ProfileInformation')).not.toBeInTheDocument()
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index 17c58ad6ddf..ece37119c4f 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -15,6 +15,7 @@ export const LOCAL_STORAGE_KEYS = {
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
DASHBOARD_PREFERENCES: (ref: string) => `dashboard-preferences-${ref}`,
+ UI_TIMEZONE: 'supabase-ui-timezone',
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_PREVIEW_UNIFIED_LOGS: 'supabase-ui-preview-unified-logs',
@@ -161,6 +162,7 @@ const LOCAL_STORAGE_KEYS_ALLOWLIST = [
LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN,
LOCAL_STORAGE_KEYS.LINTER_SHOW_FOOTER,
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
+ LOCAL_STORAGE_KEYS.UI_TIMEZONE,
]
export function clearLocalStorage() {
diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts
index df9c23ee250..827883491f9 100644
--- a/packages/common/telemetry-constants.ts
+++ b/packages/common/telemetry-constants.ts
@@ -264,6 +264,28 @@ export interface FeaturePreviewDisabledEvent {
groups: TelemetryGroups
}
+/**
+ * The user picked a timezone in the dashboard timezone picker (in the user
+ * avatar dropdown). Setting an explicit IANA value or returning to the auto
+ * detected default both fire this event.
+ *
+ * @group Events
+ * @source studio
+ */
+export interface TimezonePickerClickedEvent {
+ action: 'timezone_picker_clicked'
+ properties: {
+ /** IANA name resolved before the change. */
+ previousTimezone: string
+ /** IANA name resolved after the change. */
+ nextTimezone: string
+ /** True when the user opted back into the browser-detected default. */
+ isAutoDetected: boolean
+ /** Where the picker was rendered. */
+ source: 'user_dropdown' | 'account_preferences'
+ }
+ groups: TelemetryGroups
+}
/**
* User was exposed to the project creation form (exposure event for RLS option experiment).
*
@@ -3393,6 +3415,7 @@ export type TelemetryEvent =
| CronJobHistoryClickedEvent
| FeaturePreviewEnabledEvent
| FeaturePreviewDisabledEvent
+ | TimezonePickerClickedEvent
| ProjectCreationRlsOptionExperimentExposedEvent
| ProjectCreationDefaultPrivilegesExposedEvent
| ProjectCreationSimpleVersionSubmittedEvent
diff --git a/packages/ui-patterns/src/TimestampInfo/TimestampInfoProvider.tsx b/packages/ui-patterns/src/TimestampInfo/TimestampInfoProvider.tsx
new file mode 100644
index 00000000000..fc55770d850
--- /dev/null
+++ b/packages/ui-patterns/src/TimestampInfo/TimestampInfoProvider.tsx
@@ -0,0 +1,23 @@
+'use client'
+
+import { createContext, useContext, useMemo, type ReactNode } from 'react'
+
+interface TimestampInfoContextValue {
+ /** IANA timezone applied to TimestampInfo when no `timezone` prop is set. */
+ timezone?: string
+}
+
+const TimestampInfoContext = createContext({})
+
+export const TimestampInfoProvider = ({
+ timezone,
+ children,
+}: {
+ timezone?: string
+ children: ReactNode
+}) => {
+ const value = useMemo(() => ({ timezone }), [timezone])
+ return {children}
+}
+
+export const useTimestampInfoContext = () => useContext(TimestampInfoContext)
diff --git a/packages/ui-patterns/src/TimestampInfo/index.tsx b/packages/ui-patterns/src/TimestampInfo/index.tsx
index d9495bc6a4a..91064b904a4 100644
--- a/packages/ui-patterns/src/TimestampInfo/index.tsx
+++ b/packages/ui-patterns/src/TimestampInfo/index.tsx
@@ -2,12 +2,18 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
+import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import { useEffect, useRef, useState } from 'react'
import { cn, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { useTimestampInfoContext } from './TimestampInfoProvider'
+
+export { TimestampInfoProvider } from './TimestampInfoProvider'
+
dayjs.extend(relativeTime)
dayjs.extend(utc)
+dayjs.extend(timezone)
const unixMicroToIsoTimestamp = (unix: string | number): string => {
return dayjs.unix(Number(unix) / 1000 / 1000).toISOString()
@@ -22,11 +28,18 @@ const isUnixMicro = (unix: string | number): boolean => {
type TimestampFormatter = {
utcTimestamp: string | number
format?: string
+ /**
+ * IANA timezone to render the timestamp in. When omitted, falls back to the
+ * browser's local timezone (preserving the historical default).
+ */
+ timezone?: string
}
-export const timestampLocalFormatter = ({ utcTimestamp, format }: TimestampFormatter) => {
+export const timestampLocalFormatter = ({ utcTimestamp, format, timezone }: TimestampFormatter) => {
const timestamp = isUnixMicro(utcTimestamp) ? unixMicroToIsoTimestamp(utcTimestamp) : utcTimestamp
- return dayjs.utc(timestamp).local().format(format)
+ const base = dayjs.utc(timestamp)
+ const localised = timezone ? base.tz(timezone) : base.local()
+ return localised.format(format)
}
const timestampUtcFormatter = ({ utcTimestamp, format }: TimestampFormatter) => {
@@ -54,6 +67,7 @@ export const TimestampInfo = ({
format = 'DD MMM YY HH:mm:ss',
labelFormat = 'DD MMM YY HH:mm:ss',
label,
+ timezone: timezoneProp,
}: {
className?: string
utcTimestamp: string | number
@@ -61,13 +75,27 @@ export const TimestampInfo = ({
format?: string
labelFormat?: string
label?: string
+ /**
+ * IANA timezone to render the timestamp in. When omitted the component
+ * falls back to the value supplied by `TimestampInfoProvider`, then to the
+ * browser's local timezone.
+ */
+ timezone?: string
}) => {
- const local = timestampLocalFormatter({ utcTimestamp, format })
+ const { timezone: timezoneFromContext } = useTimestampInfoContext()
+ const effectiveTimezone = timezoneProp ?? timezoneFromContext
+ const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
+ const local = timestampLocalFormatter({ utcTimestamp, format, timezone: effectiveTimezone })
+ const browserLocal = timestampLocalFormatter({ utcTimestamp, format })
const utc = timestampUtcFormatter({ utcTimestamp, format })
const relative = timestampRelativeFormatter({ utcTimestamp })
const [align, setAlign] = useState<'start' | 'end'>('start')
const triggerRef = useRef(null)
- const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
+ const localTimezone = effectiveTimezone ?? browserTimezone
+ // Show the browser timezone as a separate row whenever the user has
+ // overridden it via the picker, so they don't lose context for "what time
+ // is it where I am" while reading logs in another timezone.
+ const showBrowserRow = !!effectiveTimezone && effectiveTimezone !== browserTimezone
// Calculate alignment based on trigger position
// Needed so that the tooltip isn't hidden behind the header on top rows (in logs)
@@ -144,13 +172,18 @@ export const TimestampInfo = ({
{label
? label
: displayAs === 'local'
- ? timestampLocalFormatter({ utcTimestamp, format: labelFormat })
+ ? timestampLocalFormatter({
+ utcTimestamp,
+ format: labelFormat,
+ timezone: effectiveTimezone,
+ })
: timestampUtcFormatter({ utcTimestamp, format: labelFormat })}
+ {showBrowserRow && }