DateRangePicker.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import dayjs from 'dayjs'
  2. import { ChevronDown } from 'lucide-react'
  3. import { ReactNode, useEffect, useState } from 'react'
  4. import {
  5. Button,
  6. cn,
  7. DropdownMenu,
  8. DropdownMenuContent,
  9. DropdownMenuRadioGroup,
  10. DropdownMenuRadioItem,
  11. DropdownMenuSeparator,
  12. DropdownMenuTrigger,
  13. } from 'ui'
  14. import { DATE_FORMAT } from '@/lib/constants'
  15. /**
  16. * There's timestamp rounding that kicks in if there are more than 50 data points
  17. * being returned, in order to increase cache hit rates.
  18. * Ensure that whenever we create a new period_start under handleChange, it should return
  19. * at point 50 data points. (e.g 3 hours, 5 mins interval = 36 points)
  20. * Otherwise there won't be any data shown on the graphs
  21. */
  22. interface DateRangePickerProps {
  23. value: string
  24. loading: boolean
  25. className?: string
  26. onChange: ({
  27. period_start,
  28. period_end,
  29. interval,
  30. }: {
  31. period_start: { date: string; time_period: string }
  32. period_end: { date: string; time_period: string }
  33. interval?: string
  34. }) => void
  35. options: { key: string; label: string; interval?: string }[]
  36. currentBillingPeriodStart?: number
  37. currentBillingPeriodEnd?: number
  38. footer?: ReactNode
  39. }
  40. export const DateRangePicker = ({
  41. onChange,
  42. value,
  43. options,
  44. className,
  45. loading,
  46. currentBillingPeriodStart,
  47. currentBillingPeriodEnd,
  48. footer,
  49. }: DateRangePickerProps) => {
  50. const [timePeriod, setTimePeriod] = useState(value)
  51. useEffect(() => {
  52. handleChange(value)
  53. }, [loading])
  54. const startOfMonth = dayjs().startOf('month').format(DATE_FORMAT)
  55. const nextMonth = dayjs().add(1, 'month').startOf('month').format(DATE_FORMAT)
  56. const today = dayjs().format(DATE_FORMAT)
  57. function handleChange(x: any) {
  58. setTimePeriod(x)
  59. switch (x) {
  60. // please note: these are fixed date ranges based on subscription
  61. // billing cycles are always assumed to be 1 month long
  62. // only currentPeriodStart is used to calculate dates
  63. case 'currentBillingCycle':
  64. onChange({
  65. period_start: {
  66. date: dayjs.unix(currentBillingPeriodStart ?? 0).format(DATE_FORMAT),
  67. time_period: '1d',
  68. },
  69. period_end: {
  70. date: dayjs.unix(currentBillingPeriodEnd ?? 0).format(DATE_FORMAT),
  71. time_period: 'today',
  72. },
  73. interval: '1d',
  74. })
  75. break
  76. case 'previousBillingCycle':
  77. onChange({
  78. period_start: {
  79. date: dayjs
  80. .unix(currentBillingPeriodStart ?? 0)
  81. .subtract(1, 'month')
  82. .format(DATE_FORMAT),
  83. time_period: '1d',
  84. },
  85. period_end: {
  86. date: dayjs.unix(currentBillingPeriodStart ?? 0).format(DATE_FORMAT),
  87. time_period: 'today',
  88. },
  89. interval: '1d',
  90. })
  91. break
  92. // all other time periods below are based on current date and time
  93. // they will generate flexible dynamic date ranges
  94. case '10m':
  95. onChange({
  96. period_start: {
  97. date: dayjs().subtract(10, 'minutes').format(DATE_FORMAT),
  98. time_period: '1d',
  99. },
  100. period_end: {
  101. date: today,
  102. time_period: 'today',
  103. },
  104. interval: '1m',
  105. })
  106. break
  107. case '30m':
  108. onChange({
  109. period_start: {
  110. date: dayjs().subtract(30, 'minutes').format(DATE_FORMAT),
  111. time_period: '1d',
  112. },
  113. period_end: {
  114. date: today,
  115. time_period: 'today',
  116. },
  117. interval: '1m',
  118. })
  119. break
  120. case '1d':
  121. onChange({
  122. period_start: {
  123. date: dayjs().subtract(24, 'hour').format(DATE_FORMAT),
  124. time_period: '1d',
  125. },
  126. period_end: {
  127. date: today,
  128. time_period: 'today',
  129. },
  130. interval: '1h',
  131. })
  132. break
  133. case '3h':
  134. onChange({
  135. period_start: {
  136. date: dayjs().subtract(3, 'hour').format(DATE_FORMAT),
  137. time_period: '3h',
  138. },
  139. period_end: {
  140. date: today,
  141. time_period: 'today',
  142. },
  143. interval: '5m',
  144. })
  145. break
  146. case '1h':
  147. onChange({
  148. period_start: {
  149. date: dayjs().subtract(1, 'hour').format(DATE_FORMAT),
  150. time_period: '1h',
  151. },
  152. period_end: {
  153. date: today,
  154. time_period: 'today',
  155. },
  156. interval: '1m',
  157. })
  158. break
  159. case '7d':
  160. onChange({
  161. period_start: {
  162. date: dayjs().subtract(7, 'day').format(DATE_FORMAT),
  163. time_period: '7d',
  164. },
  165. period_end: {
  166. date: today,
  167. time_period: 'today',
  168. },
  169. interval: '1d',
  170. })
  171. break
  172. case '30d':
  173. onChange({
  174. period_start: {
  175. date: dayjs().subtract(30, 'day').format(DATE_FORMAT),
  176. time_period: '30d',
  177. },
  178. period_end: {
  179. date: today,
  180. time_period: 'today',
  181. },
  182. interval: '1d',
  183. })
  184. break
  185. case '60d':
  186. onChange({
  187. period_start: {
  188. date: dayjs().subtract(60, 'day').format(DATE_FORMAT),
  189. time_period: '60d',
  190. },
  191. period_end: {
  192. date: today,
  193. time_period: 'today',
  194. },
  195. interval: '3d',
  196. })
  197. break
  198. case 'startMonth':
  199. onChange({
  200. period_start: {
  201. date: startOfMonth,
  202. time_period: 'startMonth',
  203. },
  204. period_end: {
  205. date: nextMonth,
  206. time_period: 'endMonth',
  207. },
  208. interval: '1d',
  209. })
  210. break
  211. default:
  212. console.warn(`Unknown period encountered: ${x}`)
  213. break
  214. }
  215. }
  216. return (
  217. <DropdownMenu>
  218. <DropdownMenuTrigger asChild>
  219. <Button type="default" iconRight={<ChevronDown />}>
  220. <span>{timePeriod && options.find((x) => x.key === timePeriod)?.label}</span>
  221. </Button>
  222. </DropdownMenuTrigger>
  223. <DropdownMenuContent side="bottom" align="start" className={cn(!footer && 'w-36', className)}>
  224. <DropdownMenuRadioGroup value={timePeriod} onValueChange={(x) => handleChange(x)}>
  225. {options.map((option) => {
  226. return (
  227. <DropdownMenuRadioItem value={option.key} key={option.key}>
  228. {option.label}
  229. </DropdownMenuRadioItem>
  230. )
  231. })}
  232. </DropdownMenuRadioGroup>
  233. {!!footer && (
  234. <>
  235. <DropdownMenuSeparator />
  236. {footer}
  237. </>
  238. )}
  239. </DropdownMenuContent>
  240. </DropdownMenu>
  241. )
  242. }
  243. export default DateRangePicker