From 1f0e5ef34085130e95fc26225fce21463a8d84ac Mon Sep 17 00:00:00 2001 From: tanjiro <56165694+NANDINI-star@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:56:24 +0900 Subject: [PATCH] New Advanced Date Range Picker Component (#13141) * new date-range picker added * remove unused utils --- .../src/components/new_usage.tsx | 6 +- .../shared/advanced_date_picker.tsx | 377 ++++++++++++++++++ 2 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index d8661002aa1..3f060fc315a 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -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 = ({ accessToken, userRole, user {/* Your Usage Panel */} - + - + diff --git a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx new file mode 100644 index 00000000000..ae60b284f5e --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx @@ -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 = ({ + value, + onValueChange, + label = "Select Time Range", + showTimeRange = true, +}) => { + const [isOpen, setIsOpen] = useState(false) + const [tempValue, setTempValue] = useState(value) + + // Custom date inputs only - removed time inputs + const [startDate, setStartDate] = useState("") + const [endDate, setEndDate] = useState("") + + const dropdownRef = useRef(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 ( +
+ {label && {label}} + +
+ {/* Main input display */} +
setIsOpen(!isOpen)} + > +
+
+ + {formatDisplayRange(value.from, value.to)} +
+ + + +
+
+ + {/* Dropdown panel */} + {isOpen && ( +
+
+ {/* Left side - Relative time options */} +
+
+ Relative time (today, 7d, 30d, MTD, YTD) +
+
+ {relativeTimeOptions.map((option) => ( +
handleRelativeTimeSelect(option)} + > + {option.label} + {option.shortLabel} +
+ ))} +
+
+ + {/* Right side - Custom date selection */} +
+
+
+ + Start and end dates +
+
+ +
+ {/* Start date */} +
+ + 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" + }`} + /> +
+ + {/* End date */} +
+ + 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" + }`} + /> +
+ + {/* Error message */} + {!validation.isValid && validation.error && ( +
+
+ + + + {validation.error} +
+
+ )} + + {/* Current selection preview */} + {tempValue.from && tempValue.to && validation.isValid && ( +
+
Preview:
+
{formatDisplayRange(tempValue.from, tempValue.to)}
+
+ )} +
+ +
+
+ + +
+
+
+
+
+ )} +
+ + {/* Time range display below */} + {showTimeRange && value.from && value.to && ( + + {moment(value.from).format("MMM D, YYYY [at] HH:mm:ss")} -{" "} + {moment(value.to).format("MMM D, YYYY [at] HH:mm:ss")} + + )} +
+ ) +} + +export default AdvancedDatePicker