From 0da56beb79c6db56720380e062359e9922939e0f Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Mon, 9 Mar 2026 11:53:07 -0700 Subject: [PATCH] feat: adding a timezone picker to the usage page, to be able to view by timezone, backend already supports this just ui change --- .../components/EntityUsage/EntityUsage.tsx | 11 +++- .../UsagePage/components/UsagePageView.tsx | 18 ++++-- .../src/components/networking.tsx | 26 +++++++-- .../shared/advanced_date_picker.tsx | 55 +++++++++++++++++++ 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx index a106910cff7..068c78032a8 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx @@ -85,9 +85,10 @@ interface EntityUsageProps { entityList: EntityList[] | null; premiumUser: boolean; dateValue: DateRangePickerValue; + timezoneOffset?: number; } -const EntityUsage: React.FC = ({ accessToken, entityType, entityId, entityList, dateValue }) => { +const EntityUsage: React.FC = ({ accessToken, entityType, entityId, entityList, dateValue, timezoneOffset }) => { const [spendData, setSpendData] = useState({ results: [], metadata: { @@ -119,6 +120,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags : null, + timezoneOffset, ); setSpendData(data); } else if (entityType === "team") { @@ -128,6 +130,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags : null, + timezoneOffset, ); setSpendData(data); } else if (entityType === "organization") { @@ -137,6 +140,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags : null, + timezoneOffset, ); setSpendData(data); } else if (entityType === "customer") { @@ -146,6 +150,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags : null, + timezoneOffset, ); setSpendData(data); } else if (entityType === "agent") { @@ -155,6 +160,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags : null, + timezoneOffset, ); setSpendData(data); } else if (entityType === "user") { @@ -164,6 +170,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti endTime, 1, selectedTags.length > 0 ? selectedTags[0] : null, + timezoneOffset, ); setSpendData(data); } else { @@ -173,7 +180,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti useEffect(() => { fetchSpendData(); - }, [accessToken, dateValue, entityId, selectedTags]); + }, [accessToken, dateValue, entityId, selectedTags, timezoneOffset]); const getTopModels = () => { const modelSpend: { [key: string]: any } = {}; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx index 8b0d5ffac05..e02d31314fc 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx @@ -78,6 +78,8 @@ const UsagePage: React.FC = ({ teams, organizations }) => { to: initialToDate, }); + const [timezoneOffset, setTimezoneOffset] = useState(() => new Date().getTimezoneOffset()); + const [allTags, setAllTags] = useState([]); const { data: customers = [] } = useCustomers(); const { data: agentsResponse } = useAgents(); @@ -378,14 +380,14 @@ const UsagePage: React.FC = ({ teams, organizations }) => { try { // Prefer aggregated endpoint to avoid many page requests try { - const aggregated = await userDailyActivityAggregatedCall(accessToken, startTime, endTime, effectiveUserId); + const aggregated = await userDailyActivityAggregatedCall(accessToken, startTime, endTime, effectiveUserId, timezoneOffset); setUserSpendData(aggregated); return; } catch (e) { // Fallback to paginated calls if aggregated endpoint is unavailable } - const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime, 1, effectiveUserId); + const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime, 1, effectiveUserId, timezoneOffset); if (firstPageData.metadata.total_pages <= 1) { setUserSpendData(firstPageData); @@ -396,7 +398,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const aggregatedMetadata = { ...firstPageData.metadata }; for (let page = 2; page <= firstPageData.metadata.total_pages; page++) { - const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page, effectiveUserId); + const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page, effectiveUserId, timezoneOffset); allResults.push(...pageData.results); if (pageData.metadata) { aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0; @@ -417,7 +419,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { setLoading(false); setIsDateChanging(false); } - }, [accessToken, dateValue.from, dateValue.to, selectedUserId, isAdmin, userID]); + }, [accessToken, dateValue.from, dateValue.to, selectedUserId, isAdmin, userID, timezoneOffset]); // Super responsive date change handler const handleDateChange = useCallback((newValue: DateRangePickerValue) => { @@ -493,7 +495,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { onChange={(value) => setUsageView(value)} isAdmin={isAdmin} /> - + {/* Your Usage Panel */} {usageView === "global" && ( @@ -825,6 +827,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { })) || null } premiumUser={premiumUser} + timezoneOffset={timezoneOffset} /> )} @@ -843,6 +846,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { } premiumUser={premiumUser} dateValue={dateValue} + timezoneOffset={timezoneOffset} /> )} @@ -861,6 +865,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { } premiumUser={premiumUser} dateValue={dateValue} + timezoneOffset={timezoneOffset} /> )} {/* Tag Usage Panel */} @@ -891,6 +896,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { entityList={allTags} premiumUser={premiumUser} dateValue={dateValue} + timezoneOffset={timezoneOffset} /> )} @@ -905,6 +911,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { } premiumUser={premiumUser} dateValue={dateValue} + timezoneOffset={timezoneOffset} /> )} {/* User Usage Panel */} @@ -917,6 +924,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { entityList={userOptions.length > 0 ? userOptions : null} premiumUser={premiumUser} dateValue={dateValue} + timezoneOffset={timezoneOffset} /> )} {/* User Agent Activity Panel */} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 3ec877f03f4..ff8a4e405c5 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1690,6 +1690,7 @@ const buildDailyActivityUrl = ( endTime: Date, page: number, extraQueryParams?: Record, + timezoneOffset?: number, ) => { const resolvedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; const baseUrl = proxyBaseUrl ? `${proxyBaseUrl}${resolvedEndpoint}` : resolvedEndpoint; @@ -1700,7 +1701,8 @@ const buildDailyActivityUrl = ( params.append("page_size", DEFAULT_DAILY_ACTIVITY_PAGE_SIZE); params.append("page", page.toString()); // Send timezone offset so backend can adjust date range for UTC storage - params.append("timezone", new Date().getTimezoneOffset().toString()); + const tz = timezoneOffset !== undefined ? timezoneOffset : new Date().getTimezoneOffset(); + params.append("timezone", tz.toString()); if (extraQueryParams) { Object.entries(extraQueryParams).forEach(([key, value]) => { @@ -1719,6 +1721,7 @@ type DailyActivityCallOptions = { endTime: Date; page?: number; extraQueryParams?: Record; + timezoneOffset?: number; }; const fetchDailyActivity = async ({ @@ -1728,9 +1731,10 @@ const fetchDailyActivity = async ({ endTime, page = 1, extraQueryParams, + timezoneOffset, }: DailyActivityCallOptions) => { try { - const url = buildDailyActivityUrl(endpoint, startTime, endTime, page, extraQueryParams); + const url = buildDailyActivityUrl(endpoint, startTime, endTime, page, extraQueryParams, timezoneOffset); const response = await fetch(url, { method: "GET", @@ -1755,7 +1759,7 @@ const fetchDailyActivity = async ({ } }; -export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1, userId: string | null = null) => { +export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1, userId: string | null = null, timezoneOffset?: number) => { /** * Get daily user activity on proxy */ @@ -1768,6 +1772,7 @@ export const userDailyActivityCall = async (accessToken: string, startTime: Date extraQueryParams: { user_id: userId, }, + timezoneOffset, }); }; @@ -1777,6 +1782,7 @@ export const tagDailyActivityCall = async ( endTime: Date, page: number = 1, tags: string[] | null = null, + timezoneOffset?: number, ) => { /** * Get daily user activity on proxy @@ -1790,6 +1796,7 @@ export const tagDailyActivityCall = async ( extraQueryParams: { tags, }, + timezoneOffset, }); }; @@ -1799,6 +1806,7 @@ export const teamDailyActivityCall = async ( endTime: Date, page: number = 1, teamIds: string[] | null = null, + timezoneOffset?: number, ) => { /** * Get daily user activity on proxy @@ -1813,6 +1821,7 @@ export const teamDailyActivityCall = async ( team_ids: teamIds, exclude_team_ids: "litellm-dashboard", }, + timezoneOffset, }); }; @@ -1822,6 +1831,7 @@ export const organizationDailyActivityCall = async ( endTime: Date, page: number = 1, organizationIds: string[] | null = null, + timezoneOffset?: number, ) => { return fetchDailyActivity({ accessToken, @@ -1832,6 +1842,7 @@ export const organizationDailyActivityCall = async ( extraQueryParams: { organization_ids: organizationIds, }, + timezoneOffset, }); }; @@ -1841,6 +1852,7 @@ export const customerDailyActivityCall = async ( endTime: Date, page: number = 1, customerIds: string[] | null = null, + timezoneOffset?: number, ) => { return fetchDailyActivity({ accessToken, @@ -1851,6 +1863,7 @@ export const customerDailyActivityCall = async ( extraQueryParams: { end_user_ids: customerIds, }, + timezoneOffset, }); }; @@ -1860,6 +1873,7 @@ export const agentDailyActivityCall = async ( endTime: Date, page: number = 1, agentIds: string[] | null = null, + timezoneOffset?: number, ) => { return fetchDailyActivity({ accessToken, @@ -1870,6 +1884,7 @@ export const agentDailyActivityCall = async ( extraQueryParams: { agent_ids: agentIds, }, + timezoneOffset, }); }; @@ -3173,7 +3188,7 @@ export const keyAliasesCall = async ( } }; -export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null) => { +export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null, timezoneOffset?: number) => { /** * Get aggregated daily user activity (no pagination) */ @@ -3190,7 +3205,8 @@ export const userDailyActivityAggregatedCall = async (accessToken: string, start queryParams.append("start_date", formatDate(startTime)); queryParams.append("end_date", formatDate(endTime)); // Send timezone offset so backend can adjust date range for UTC storage - queryParams.append("timezone", new Date().getTimezoneOffset().toString()); + const tz = timezoneOffset !== undefined ? timezoneOffset : new Date().getTimezoneOffset(); + queryParams.append("timezone", tz.toString()); if (userId) { queryParams.append("user_id", userId); } diff --git a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx index 4293657b71a..97703f53ea9 100644 --- a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx +++ b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx @@ -9,8 +9,46 @@ interface AdvancedDatePickerProps { label?: string; className?: string; showTimeRange?: boolean; + timezoneOffset?: number; + onTimezoneChange?: (offset: number) => void; } +const getLocalTimezoneOffset = (): number => new Date().getTimezoneOffset(); + +const formatTimezoneLabel = (offsetMinutes: number): string => { + if (offsetMinutes === 0) return "UTC"; + const sign = offsetMinutes > 0 ? "-" : "+"; + const absMinutes = Math.abs(offsetMinutes); + const hours = Math.floor(absMinutes / 60); + const minutes = absMinutes % 60; + const formatted = minutes > 0 ? `${hours}:${String(minutes).padStart(2, "0")}` : `${hours}`; + return `UTC${sign}${formatted}`; +}; + +// Common timezone options: offset in minutes (JS getTimezoneOffset convention: positive = west of UTC) +const TIMEZONE_OPTIONS: { label: string; offset: number }[] = [ + { label: "UTC-10 (Hawaii)", offset: 600 }, + { label: "UTC-9 (Alaska)", offset: 540 }, + { label: "UTC-8 (Pacific)", offset: 480 }, + { label: "UTC-7 (Mountain)", offset: 420 }, + { label: "UTC-6 (Central)", offset: 360 }, + { label: "UTC-5 (Eastern)", offset: 300 }, + { label: "UTC-4 (Atlantic)", offset: 240 }, + { label: "UTC-3 (Buenos Aires)", offset: 180 }, + { label: "UTC (London/UTC)", offset: 0 }, + { label: "UTC+1 (Central Europe)", offset: -60 }, + { label: "UTC+2 (Eastern Europe)", offset: -120 }, + { label: "UTC+3 (Moscow)", offset: -180 }, + { label: "UTC+4 (Dubai)", offset: -240 }, + { label: "UTC+5 (Pakistan)", offset: -300 }, + { label: "UTC+5:30 (India)", offset: -330 }, + { label: "UTC+7 (Bangkok)", offset: -420 }, + { label: "UTC+8 (Singapore)", offset: -480 }, + { label: "UTC+9 (Japan/Korea)", offset: -540 }, + { label: "UTC+10 (Sydney)", offset: -600 }, + { label: "UTC+12 (Auckland)", offset: -720 }, +]; + interface RelativeTimeOption { label: string; shortLabel: string; @@ -68,6 +106,8 @@ const AdvancedDatePicker: React.FC = ({ onValueChange, label = "Select Time Range", showTimeRange = true, + timezoneOffset, + onTimezoneChange, }) => { const [isOpen, setIsOpen] = useState(false); const [tempValue, setTempValue] = useState(value); @@ -420,6 +460,21 @@ const AdvancedDatePicker: React.FC = ({ )} + {/* Timezone selector */} + {onTimezoneChange && ( + + )} ); };