mirror of
https://github.com/supabase/supabase.git
synced 2026-06-17 21:23:59 +08:00
## Problem Our `<Button>` component breaks the default `button` contract by redefining the `type` prop to set its variant (`primary`, `default`, etc) instead of the button type (`submit`, `button`, etc). This is confusing and forces to write more code when using it with shadcn components that expect/inject the standard button props. ## Solution - rename the `type` prop to `variant` - rename the `htmlType` prop to `type` - propagate the changes where necessary - format code ## How to test As this is just prop renaming, if it builds it's ok --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
256 lines
6.7 KiB
TypeScript
256 lines
6.7 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { ChevronDown } from 'lucide-react'
|
|
import { ReactNode, useEffect, useState } from 'react'
|
|
import {
|
|
Button,
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from 'ui'
|
|
|
|
import { DATE_FORMAT } from '@/lib/constants'
|
|
|
|
/**
|
|
* There's timestamp rounding that kicks in if there are more than 50 data points
|
|
* being returned, in order to increase cache hit rates.
|
|
* Ensure that whenever we create a new period_start under handleChange, it should return
|
|
* at point 50 data points. (e.g 3 hours, 5 mins interval = 36 points)
|
|
* Otherwise there won't be any data shown on the graphs
|
|
*/
|
|
|
|
interface DateRangePickerProps {
|
|
value: string
|
|
loading: boolean
|
|
className?: string
|
|
onChange: ({
|
|
period_start,
|
|
period_end,
|
|
interval,
|
|
}: {
|
|
period_start: { date: string; time_period: string }
|
|
period_end: { date: string; time_period: string }
|
|
interval?: string
|
|
}) => void
|
|
options: { key: string; label: string; interval?: string }[]
|
|
currentBillingPeriodStart?: number
|
|
currentBillingPeriodEnd?: number
|
|
footer?: ReactNode
|
|
}
|
|
|
|
export const DateRangePicker = ({
|
|
onChange,
|
|
value,
|
|
options,
|
|
className,
|
|
loading,
|
|
currentBillingPeriodStart,
|
|
currentBillingPeriodEnd,
|
|
footer,
|
|
}: DateRangePickerProps) => {
|
|
const [timePeriod, setTimePeriod] = useState(value)
|
|
|
|
useEffect(() => {
|
|
handleChange(value)
|
|
}, [loading])
|
|
|
|
const startOfMonth = dayjs().startOf('month').format(DATE_FORMAT)
|
|
const nextMonth = dayjs().add(1, 'month').startOf('month').format(DATE_FORMAT)
|
|
|
|
const today = dayjs().format(DATE_FORMAT)
|
|
|
|
function handleChange(x: any) {
|
|
setTimePeriod(x)
|
|
|
|
switch (x) {
|
|
// please note: these are fixed date ranges based on subscription
|
|
// billing cycles are always assumed to be 1 month long
|
|
// only currentPeriodStart is used to calculate dates
|
|
case 'currentBillingCycle':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs.unix(currentBillingPeriodStart ?? 0).format(DATE_FORMAT),
|
|
time_period: '1d',
|
|
},
|
|
period_end: {
|
|
date: dayjs.unix(currentBillingPeriodEnd ?? 0).format(DATE_FORMAT),
|
|
time_period: 'today',
|
|
},
|
|
interval: '1d',
|
|
})
|
|
break
|
|
case 'previousBillingCycle':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs
|
|
.unix(currentBillingPeriodStart ?? 0)
|
|
.subtract(1, 'month')
|
|
.format(DATE_FORMAT),
|
|
time_period: '1d',
|
|
},
|
|
period_end: {
|
|
date: dayjs.unix(currentBillingPeriodStart ?? 0).format(DATE_FORMAT),
|
|
time_period: 'today',
|
|
},
|
|
interval: '1d',
|
|
})
|
|
break
|
|
// all other time periods below are based on current date and time
|
|
// they will generate flexible dynamic date ranges
|
|
case '10m':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(10, 'minutes').format(DATE_FORMAT),
|
|
time_period: '1d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1m',
|
|
})
|
|
break
|
|
case '30m':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(30, 'minutes').format(DATE_FORMAT),
|
|
time_period: '1d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1m',
|
|
})
|
|
break
|
|
case '1d':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(24, 'hour').format(DATE_FORMAT),
|
|
time_period: '1d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1h',
|
|
})
|
|
break
|
|
case '3h':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(3, 'hour').format(DATE_FORMAT),
|
|
time_period: '3h',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '5m',
|
|
})
|
|
break
|
|
case '1h':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(1, 'hour').format(DATE_FORMAT),
|
|
time_period: '1h',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1m',
|
|
})
|
|
break
|
|
case '7d':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(7, 'day').format(DATE_FORMAT),
|
|
time_period: '7d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1d',
|
|
})
|
|
break
|
|
case '30d':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(30, 'day').format(DATE_FORMAT),
|
|
time_period: '30d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '1d',
|
|
})
|
|
break
|
|
case '60d':
|
|
onChange({
|
|
period_start: {
|
|
date: dayjs().subtract(60, 'day').format(DATE_FORMAT),
|
|
time_period: '60d',
|
|
},
|
|
period_end: {
|
|
date: today,
|
|
time_period: 'today',
|
|
},
|
|
interval: '3d',
|
|
})
|
|
break
|
|
case 'startMonth':
|
|
onChange({
|
|
period_start: {
|
|
date: startOfMonth,
|
|
time_period: 'startMonth',
|
|
},
|
|
period_end: {
|
|
date: nextMonth,
|
|
time_period: 'endMonth',
|
|
},
|
|
interval: '1d',
|
|
})
|
|
break
|
|
|
|
default:
|
|
console.warn(`Unknown period encountered: ${x}`)
|
|
break
|
|
}
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="default" iconRight={<ChevronDown />}>
|
|
<span>{timePeriod && options.find((x) => x.key === timePeriod)?.label}</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="bottom" align="start" className={cn(!footer && 'w-36', className)}>
|
|
<DropdownMenuRadioGroup value={timePeriod} onValueChange={(x) => handleChange(x)}>
|
|
{options.map((option) => {
|
|
return (
|
|
<DropdownMenuRadioItem value={option.key} key={option.key}>
|
|
{option.label}
|
|
</DropdownMenuRadioItem>
|
|
)
|
|
})}
|
|
</DropdownMenuRadioGroup>
|
|
{!!footer && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
{footer}
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
|
|
export default DateRangePicker
|