DateRangePicker.tsx255 lines · main
| 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 | |
| 15 | import { 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 | |
| 25 | interface 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 | |
| 44 | export 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 | |
| 255 | export default DateRangePicker |