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