mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
New Advanced Date Range Picker Component (#13141)
* new date-range picker added * remove unused utils
This commit is contained in:
parent
89daa665a3
commit
1f0e5ef340
2 changed files with 380 additions and 3 deletions
|
|
@ -31,7 +31,7 @@ import {
|
|||
DateRangePickerValue,
|
||||
Button,
|
||||
} from "@tremor/react"
|
||||
import UsageDatePicker from "./shared/usage_date_picker"
|
||||
import AdvancedDatePicker from "./shared/advanced_date_picker"
|
||||
import { AreaChart } from "@tremor/react"
|
||||
|
||||
import { userDailyActivityCall, tagListCall } from "./networking"
|
||||
|
|
@ -431,9 +431,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, user
|
|||
<TabPanels>
|
||||
{/* Your Usage Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={2} className="gap-2 w-full mb-4">
|
||||
<Grid numItems={2} className="gap-10 w-1/2 mb-4">
|
||||
<Col>
|
||||
<UsageDatePicker value={dateValue} onValueChange={handleDateChange} />
|
||||
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
|
||||
</Col>
|
||||
</Grid>
|
||||
<TabGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
import React, { useCallback, useState, useRef, useEffect } from "react"
|
||||
import { DateRangePickerValue, Text, Button } from "@tremor/react"
|
||||
import { CalendarOutlined, ClockCircleOutlined } from "@ant-design/icons"
|
||||
import moment from "moment"
|
||||
|
||||
interface AdvancedDatePickerProps {
|
||||
value: DateRangePickerValue
|
||||
onValueChange: (value: DateRangePickerValue) => void
|
||||
label?: string
|
||||
className?: string
|
||||
showTimeRange?: boolean
|
||||
}
|
||||
|
||||
interface RelativeTimeOption {
|
||||
label: string
|
||||
shortLabel: string
|
||||
getValue: () => { from: Date; to: Date }
|
||||
}
|
||||
|
||||
const relativeTimeOptions: RelativeTimeOption[] = [
|
||||
{
|
||||
label: "Today",
|
||||
shortLabel: "today",
|
||||
getValue: () => ({
|
||||
from: moment().startOf("day").toDate(),
|
||||
to: moment().endOf("day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
shortLabel: "7d",
|
||||
getValue: () => ({
|
||||
from: moment().subtract(7, "days").startOf("day").toDate(),
|
||||
to: moment().endOf("day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
shortLabel: "30d",
|
||||
getValue: () => ({
|
||||
from: moment().subtract(30, "days").startOf("day").toDate(),
|
||||
to: moment().endOf("day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Month to date",
|
||||
shortLabel: "MTD",
|
||||
getValue: () => ({
|
||||
from: moment().startOf("month").toDate(),
|
||||
to: moment().endOf("day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Year to date",
|
||||
shortLabel: "YTD",
|
||||
getValue: () => ({
|
||||
from: moment().startOf("year").toDate(),
|
||||
to: moment().endOf("day").toDate(),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Advanced Date Range Picker with dropdown, relative times, and custom inputs
|
||||
*/
|
||||
const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
|
||||
value,
|
||||
onValueChange,
|
||||
label = "Select Time Range",
|
||||
showTimeRange = true,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [tempValue, setTempValue] = useState<DateRangePickerValue>(value)
|
||||
|
||||
// Custom date inputs only - removed time inputs
|
||||
const [startDate, setStartDate] = useState("")
|
||||
const [endDate, setEndDate] = useState("")
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Validation logic - simplified for dates only
|
||||
const validateDateRange = useCallback(() => {
|
||||
if (!startDate || !endDate) {
|
||||
return { isValid: true, error: "" }
|
||||
}
|
||||
|
||||
const start = moment(startDate, "YYYY-MM-DD")
|
||||
const end = moment(endDate, "YYYY-MM-DD")
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return { isValid: false, error: "Invalid date format" }
|
||||
}
|
||||
|
||||
if (end.isBefore(start)) {
|
||||
return { isValid: false, error: "End date cannot be before start date" }
|
||||
}
|
||||
|
||||
return { isValid: true, error: "" }
|
||||
}, [startDate, endDate])
|
||||
|
||||
const validation = validateDateRange()
|
||||
|
||||
// Initialize form inputs when component mounts or value changes
|
||||
useEffect(() => {
|
||||
if (value.from) {
|
||||
setStartDate(moment(value.from).format("YYYY-MM-DD"))
|
||||
}
|
||||
if (value.to) {
|
||||
setEndDate(moment(value.to).format("YYYY-MM-DD"))
|
||||
}
|
||||
setTempValue(value)
|
||||
}, [value])
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const formatDisplayRange = useCallback((from: Date | undefined, to: Date | undefined) => {
|
||||
if (!from || !to) return "Select date range"
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return moment(date).format("D MMM, HH:mm")
|
||||
}
|
||||
|
||||
return `${formatDateTime(from)} - ${formatDateTime(to)}`
|
||||
}, [])
|
||||
|
||||
// CRITICAL: Apply the same date adjustment logic as the original component
|
||||
const adjustDateRange = useCallback((newValue: DateRangePickerValue): DateRangePickerValue => {
|
||||
if (!newValue.from) return newValue
|
||||
|
||||
const adjustedValue = { ...newValue }
|
||||
const adjustedStartTime = new Date(newValue.from)
|
||||
let adjustedEndTime: Date
|
||||
|
||||
if (newValue.to) {
|
||||
adjustedEndTime = new Date(newValue.to)
|
||||
} else {
|
||||
adjustedEndTime = new Date(newValue.from)
|
||||
}
|
||||
|
||||
const isSameDay = adjustedStartTime.toDateString() === adjustedEndTime.toDateString()
|
||||
|
||||
if (isSameDay) {
|
||||
adjustedStartTime.setHours(0, 0, 0, 0)
|
||||
adjustedEndTime.setHours(23, 59, 59, 999)
|
||||
} else {
|
||||
adjustedStartTime.setHours(0, 0, 0, 0)
|
||||
adjustedEndTime.setHours(23, 59, 59, 999)
|
||||
}
|
||||
|
||||
adjustedValue.from = adjustedStartTime
|
||||
adjustedValue.to = adjustedEndTime
|
||||
|
||||
return adjustedValue
|
||||
}, [])
|
||||
|
||||
const handleRelativeTimeSelect = (option: RelativeTimeOption) => {
|
||||
const { from, to } = option.getValue()
|
||||
const newValue = { from, to }
|
||||
setTempValue(newValue)
|
||||
|
||||
// Update the form inputs to reflect the selection
|
||||
setStartDate(moment(from).format("YYYY-MM-DD"))
|
||||
setEndDate(moment(to).format("YYYY-MM-DD"))
|
||||
}
|
||||
|
||||
const updateTempValueFromInputs = useCallback(() => {
|
||||
try {
|
||||
if (startDate && endDate && validation.isValid) {
|
||||
// Set times to start and end of day
|
||||
const from = moment(startDate, "YYYY-MM-DD").startOf("day")
|
||||
const to = moment(endDate, "YYYY-MM-DD").endOf("day")
|
||||
|
||||
if (from.isValid() && to.isValid()) {
|
||||
setTempValue({ from: from.toDate(), to: to.toDate() })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Invalid date format:", error)
|
||||
}
|
||||
}, [startDate, endDate, validation.isValid])
|
||||
|
||||
// Update tempValue when inputs change
|
||||
useEffect(() => {
|
||||
updateTempValueFromInputs()
|
||||
}, [updateTempValueFromInputs])
|
||||
|
||||
const handleApply = () => {
|
||||
if (tempValue.from && tempValue.to && validation.isValid) {
|
||||
// First call with immediate value for UI responsiveness
|
||||
onValueChange(tempValue)
|
||||
|
||||
// Then do the same background adjustment logic as the original component
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
const adjustedValue = adjustDateRange(tempValue)
|
||||
onValueChange(adjustedValue)
|
||||
},
|
||||
{ timeout: 100 },
|
||||
)
|
||||
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
// Reset to original value
|
||||
setTempValue(value)
|
||||
|
||||
// Reset form inputs
|
||||
if (value.from) {
|
||||
setStartDate(moment(value.from).format("YYYY-MM-DD"))
|
||||
}
|
||||
if (value.to) {
|
||||
setEndDate(moment(value.to).format("YYYY-MM-DD"))
|
||||
}
|
||||
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <Text className="mb-2">{label}</Text>}
|
||||
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{/* Main input display */}
|
||||
<div
|
||||
className="w-[300px] px-3 py-2 text-sm border border-gray-300 rounded-md bg-white cursor-pointer hover:border-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockCircleOutlined className="text-gray-600" />
|
||||
<span className="text-gray-900">{formatDisplayRange(value.from, value.to)}</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown panel */}
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 z-[9999] min-w-[600px] mt-1 bg-white border border-gray-200 rounded-lg shadow-xl">
|
||||
<div className="flex">
|
||||
{/* Left side - Relative time options */}
|
||||
<div className="w-1/2 border-r border-gray-200">
|
||||
<div className="p-3 border-b border-gray-200">
|
||||
<span className="text-sm font-semibold text-gray-900">Relative time (today, 7d, 30d, MTD, YTD)</span>
|
||||
</div>
|
||||
<div className="h-[500px] overflow-y-auto">
|
||||
{relativeTimeOptions.map((option) => (
|
||||
<div
|
||||
key={option.label}
|
||||
className="flex items-center justify-between px-5 py-4 hover:bg-gray-50 cursor-pointer border-b border-gray-100"
|
||||
onClick={() => handleRelativeTimeSelect(option)}
|
||||
>
|
||||
<span className="text-sm text-gray-700">{option.label}</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">{option.shortLabel}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Custom date selection */}
|
||||
<div className="w-1/2 relative">
|
||||
<div className="p-3.5 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarOutlined className="text-gray-600" />
|
||||
<span className="text-sm font-semibold text-gray-900">Start and end dates</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6 pb-20">
|
||||
{/* Start date */}
|
||||
<div>
|
||||
<label className="text-sm text-gray-700 mb-1 block">Start date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
|
||||
!validation.isValid
|
||||
? "border-red-300 focus:border-red-500 focus:ring-red-200"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* End date */}
|
||||
<div>
|
||||
<label className="text-sm text-gray-700 mb-1 block">End date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
|
||||
!validation.isValid
|
||||
? "border-red-300 focus:border-red-500 focus:ring-red-200"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{!validation.isValid && validation.error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-red-700 font-medium">{validation.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current selection preview */}
|
||||
{tempValue.from && tempValue.to && validation.isValid && (
|
||||
<div className="bg-blue-50 p-3 rounded-md">
|
||||
<div className="text-xs text-blue-700 font-medium">Preview:</div>
|
||||
<div className="text-sm text-blue-800">{formatDisplayRange(tempValue.from, tempValue.to)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleApply} disabled={!tempValue.from || !tempValue.to || !validation.isValid}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time range display below */}
|
||||
{showTimeRange && value.from && value.to && (
|
||||
<Text className="mt-2 text-xs text-gray-500">
|
||||
{moment(value.from).format("MMM D, YYYY [at] HH:mm:ss")} -{" "}
|
||||
{moment(value.to).format("MMM D, YYYY [at] HH:mm:ss")}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvancedDatePicker
|
||||
Loading…
Add table
Reference in a new issue