TimezoneSettings.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { useFlag } from 'common'
  2. import { CheckIcon, ChevronsUpDown, Globe } from 'lucide-react'
  3. import { useId, useMemo, useState } from 'react'
  4. import {
  5. Button,
  6. Card,
  7. CardContent,
  8. cn,
  9. Command,
  10. CommandEmpty,
  11. CommandGroup,
  12. CommandInput,
  13. CommandItem,
  14. CommandList,
  15. Popover,
  16. PopoverContent,
  17. PopoverTrigger,
  18. ScrollArea,
  19. } from 'ui'
  20. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  21. import {
  22. PageSection,
  23. PageSectionContent,
  24. PageSectionDescription,
  25. PageSectionMeta,
  26. PageSectionSummary,
  27. PageSectionTitle,
  28. } from 'ui-patterns/PageSection'
  29. import { findTimezoneByIana, TIMEZONES_BY_IANA } from '@/lib/constants/timezones'
  30. import { useTimezone } from '@/lib/datetime'
  31. import { guessLocalTimezone } from '@/lib/dayjs'
  32. import { useTrack } from '@/lib/telemetry/track'
  33. const AUTO_OPTION_VALUE = '__auto__'
  34. export const TimezoneSettings = () => {
  35. const timezonePickerEnabled = useFlag('timezonePicker')
  36. const { timezone, storedTimezone, setTimezone, isAutoDetected } = useTimezone()
  37. const track = useTrack()
  38. const [open, setOpen] = useState(false)
  39. const listboxId = useId()
  40. // Browser timezone is captured once and stays stable even when the user has
  41. // overridden the dashboard timezone — that's the value the "Auto detect"
  42. // option will revert to.
  43. const browserTimezone = useMemo(() => guessLocalTimezone(), [])
  44. const triggerLabel = useMemo(() => findTimezoneByIana(timezone)?.text ?? timezone, [timezone])
  45. if (!timezonePickerEnabled) return null
  46. const handleSelect = (nextStored: string) => {
  47. setTimezone(nextStored)
  48. const resolvedNext = nextStored || guessLocalTimezone()
  49. track('timezone_picker_clicked', {
  50. previousTimezone: timezone,
  51. nextTimezone: resolvedNext,
  52. isAutoDetected: nextStored === '',
  53. source: 'account_preferences',
  54. })
  55. setOpen(false)
  56. }
  57. return (
  58. <PageSection>
  59. <PageSectionMeta>
  60. <PageSectionSummary>
  61. <PageSectionTitle>Timezone</PageSectionTitle>
  62. <PageSectionDescription>
  63. Choose how dates and times in logs and other dashboard surfaces are displayed.
  64. </PageSectionDescription>
  65. </PageSectionSummary>
  66. </PageSectionMeta>
  67. <PageSectionContent>
  68. <Card>
  69. <CardContent>
  70. <FormItemLayout
  71. isReactForm={false}
  72. label="Display timezone"
  73. layout="flex-row-reverse"
  74. description={
  75. isAutoDetected
  76. ? `Auto detected from your browser (${browserTimezone}).`
  77. : 'Pick "Auto detect" to follow your browser timezone again.'
  78. }
  79. >
  80. <Popover open={open} onOpenChange={setOpen}>
  81. <PopoverTrigger asChild>
  82. <Button
  83. role="combobox"
  84. aria-expanded={open}
  85. aria-controls={listboxId}
  86. className="w-full justify-between"
  87. type="default"
  88. size="small"
  89. icon={<Globe />}
  90. iconRight={<ChevronsUpDown size={14} strokeWidth={1.5} />}
  91. >
  92. <span className="truncate text-left">
  93. {isAutoDetected ? `Auto detect (${timezone})` : triggerLabel}
  94. </span>
  95. </Button>
  96. </PopoverTrigger>
  97. <PopoverContent id={listboxId} className="w-[--radix-popover-trigger-width] p-0">
  98. <Command>
  99. <CommandInput placeholder="Search timezone..." className="h-9" />
  100. <CommandList>
  101. <CommandEmpty>No timezones found</CommandEmpty>
  102. <CommandGroup>
  103. <ScrollArea className="h-72">
  104. <CommandItem
  105. key={AUTO_OPTION_VALUE}
  106. value={`Auto detect ${browserTimezone}`}
  107. onSelect={() => handleSelect('')}
  108. >
  109. <div className="flex flex-col">
  110. <span>Auto detect</span>
  111. <span className="text-xs text-foreground-lighter">
  112. {browserTimezone}
  113. </span>
  114. </div>
  115. <CheckIcon
  116. className={cn(
  117. 'ml-auto h-4 w-4',
  118. isAutoDetected ? 'opacity-100' : 'opacity-0'
  119. )}
  120. />
  121. </CommandItem>
  122. {TIMEZONES_BY_IANA.map((entry) => {
  123. const ianaName = entry.utc[0]
  124. const isSelected = !isAutoDetected && storedTimezone === ianaName
  125. return (
  126. <CommandItem
  127. key={ianaName}
  128. // CommandItem matches against the `value` prop for the input filter — include
  129. // both the human label and the IANA name so search works for either.
  130. value={`${entry.text} ${ianaName}`}
  131. onSelect={() => handleSelect(ianaName)}
  132. >
  133. {entry.text}
  134. <CheckIcon
  135. className={cn(
  136. 'ml-auto h-4 w-4',
  137. isSelected ? 'opacity-100' : 'opacity-0'
  138. )}
  139. />
  140. </CommandItem>
  141. )
  142. })}
  143. </ScrollArea>
  144. </CommandGroup>
  145. </CommandList>
  146. </Command>
  147. </PopoverContent>
  148. </Popover>
  149. </FormItemLayout>
  150. </CardContent>
  151. </Card>
  152. </PageSectionContent>
  153. </PageSection>
  154. )
  155. }