Skip to content

Commit 98ba5c8

Browse files
Merge pull request #12042 from colesmcintosh/fix-today-selector-date-mutation-bug
Fix today selector date mutation bug in dashboard components
2 parents e556071 + 8f376a6 commit 98ba5c8

File tree

8 files changed

+172
-61
lines changed

8 files changed

+172
-61
lines changed

ui/litellm-dashboard/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/litellm-dashboard/src/components/cache_dashboard.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Icon,
2323
Text,
2424
} from "@tremor/react";
25+
import UsageDatePicker from "./shared/usage_date_picker";
2526

2627
import {
2728
Button as Button2,
@@ -161,12 +162,6 @@ const CacheDashboard: React.FC<CachePageProps> = ({
161162
return;
162163
}
163164

164-
// the endTime put it to the last hour of the selected date
165-
endTime.setHours(23, 59, 59, 999);
166-
167-
// startTime put it to the first hour of the selected date
168-
startTime.setHours(0, 0, 0, 0);
169-
170165
let new_cache_data = await adminGlobalCacheActivity(
171166
accessToken,
172167
formatDateWithoutTZ(startTime),
@@ -349,14 +344,12 @@ const runCachingHealthCheck = async () => {
349344
</MultiSelect>
350345
</Col>
351346
<Col>
352-
<DateRangePicker
353-
enableSelect={true}
347+
<UsageDatePicker
354348
value={dateValue}
355349
onValueChange={(value) => {
356350
setDateValue(value);
357351
updateCachingData(value.from, value.to);
358352
}}
359-
selectPlaceholder="Select date range"
360353
/>
361354
</Col>
362355
</Grid>

ui/litellm-dashboard/src/components/entity_usage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DonutChart,
77
TabPanel, TabGroup, TabList, Tab, TabPanels
88
} from "@tremor/react";
9+
import UsageDatePicker from "./shared/usage_date_picker";
910
import { Select } from 'antd';
1011
import { ActivityMetrics, processActivityData } from './activity_metrics';
1112
import { DailyData, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types';
@@ -94,8 +95,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
9495

9596
const fetchSpendData = async () => {
9697
if (!accessToken || !dateValue.from || !dateValue.to) return;
97-
const startTime = dateValue.from;
98-
const endTime = dateValue.to;
98+
// Create new Date objects to avoid mutating the original dates
99+
const startTime = new Date(dateValue.from);
100+
const endTime = new Date(dateValue.to);
99101

100102
if (entityType === 'tag') {
101103
const data = await tagDailyActivityCall(
@@ -287,9 +289,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
287289
<div style={{ width: "100%" }}>
288290
<Grid numItems={2} className="gap-2 w-full mb-4">
289291
<Col>
290-
<Text>Select Time Range</Text>
291-
<DateRangePicker
292-
enableSelect={true}
292+
<UsageDatePicker
293293
value={dateValue}
294294
onValueChange={setDateValue}
295295
/>

ui/litellm-dashboard/src/components/model_dashboard.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
MultiSelectItem,
4444
DateRangePickerValue,
4545
} from "@tremor/react";
46+
import UsageDatePicker from "./shared/usage_date_picker";
4647
import {
4748
modelInfoCall,
4849
userGetRequesedtModelsCall,
@@ -306,15 +307,6 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
306307
selected_customer = null;
307308
}
308309

309-
// make startTime and endTime to last hour of the day
310-
startTime.setHours(0);
311-
startTime.setMinutes(0);
312-
startTime.setSeconds(0);
313-
314-
endTime.setHours(23);
315-
endTime.setMinutes(59);
316-
endTime.setSeconds(59);
317-
318310
try {
319311
const modelMetricsResponse = await modelMetricsCall(
320312
accessToken,
@@ -1387,9 +1379,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
13871379
<TabPanel>
13881380
<Grid numItems={4} className="mt-2 mb-2">
13891381
<Col>
1390-
<Text>Select Time Range</Text>
1391-
<DateRangePicker
1392-
enableSelect={true}
1382+
<UsageDatePicker
13931383
value={dateValue}
13941384
className="mr-2"
13951385
onValueChange={(value) => {
@@ -1398,7 +1388,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
13981388
selectedModelGroup,
13991389
value.from,
14001390
value.to
1401-
); // Call updateModelMetrics with the new date range
1391+
);
14021392
}}
14031393
/>
14041394
</Col>

ui/litellm-dashboard/src/components/networking.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from "./email_events/types";
2020

2121
const isLocal = process.env.NODE_ENV === "development";
22-
export const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : null;
22+
export const defaultProxyBaseUrl = isLocal ? "http://localhost:43845" : null;
2323
const defaultServerRootPath = "/";
2424
export let serverRootPath = defaultServerRootPath;
2525
export let proxyBaseUrl = defaultProxyBaseUrl;
@@ -1333,8 +1333,15 @@ export const userDailyActivityCall = async (
13331333
? `${proxyBaseUrl}/user/daily/activity`
13341334
: `/user/daily/activity`;
13351335
const queryParams = new URLSearchParams();
1336-
queryParams.append("start_date", startTime.toISOString());
1337-
queryParams.append("end_date", endTime.toISOString());
1336+
// Format dates as YYYY-MM-DD for the API
1337+
const formatDate = (date: Date) => {
1338+
const year = date.getFullYear();
1339+
const month = String(date.getMonth() + 1).padStart(2, '0');
1340+
const day = String(date.getDate()).padStart(2, '0');
1341+
return `${year}-${month}-${day}`;
1342+
};
1343+
queryParams.append("start_date", formatDate(startTime));
1344+
queryParams.append("end_date", formatDate(endTime));
13381345
queryParams.append("page_size", "1000");
13391346
queryParams.append("page", page.toString());
13401347
const queryString = queryParams.toString();
@@ -1379,8 +1386,15 @@ export const tagDailyActivityCall = async (
13791386
? `${proxyBaseUrl}/tag/daily/activity`
13801387
: `/tag/daily/activity`;
13811388
const queryParams = new URLSearchParams();
1382-
queryParams.append("start_date", startTime.toISOString());
1383-
queryParams.append("end_date", endTime.toISOString());
1389+
// Format dates as YYYY-MM-DD for the API
1390+
const formatDate = (date: Date) => {
1391+
const year = date.getFullYear();
1392+
const month = String(date.getMonth() + 1).padStart(2, '0');
1393+
const day = String(date.getDate()).padStart(2, '0');
1394+
return `${year}-${month}-${day}`;
1395+
};
1396+
queryParams.append("start_date", formatDate(startTime));
1397+
queryParams.append("end_date", formatDate(endTime));
13841398
queryParams.append("page_size", "1000");
13851399
queryParams.append("page", page.toString());
13861400
if (tags) {
@@ -1428,8 +1442,15 @@ export const teamDailyActivityCall = async (
14281442
? `${proxyBaseUrl}/team/daily/activity`
14291443
: `/team/daily/activity`;
14301444
const queryParams = new URLSearchParams();
1431-
queryParams.append("start_date", startTime.toISOString());
1432-
queryParams.append("end_date", endTime.toISOString());
1445+
// Format dates as YYYY-MM-DD for the API
1446+
const formatDate = (date: Date) => {
1447+
const year = date.getFullYear();
1448+
const month = String(date.getMonth() + 1).padStart(2, '0');
1449+
const day = String(date.getDate()).padStart(2, '0');
1450+
return `${year}-${month}-${day}`;
1451+
};
1452+
queryParams.append("start_date", formatDate(startTime));
1453+
queryParams.append("end_date", formatDate(endTime));
14331454
queryParams.append("page_size", "1000");
14341455
queryParams.append("page", page.toString());
14351456
if (teamIds) {

ui/litellm-dashboard/src/components/new_usage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
TableHeaderCell, TableBody, TableCell,
1616
Subtitle, DateRangePicker, DateRangePickerValue
1717
} from "@tremor/react";
18+
import UsageDatePicker from "./shared/usage_date_picker";
1819
import { AreaChart } from "@tremor/react";
1920

2021
import { userDailyActivityCall, tagListCall } from "./networking";
@@ -211,8 +212,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
211212

212213
const fetchUserSpendData = async () => {
213214
if (!accessToken || !dateValue.from || !dateValue.to) return;
214-
const startTime = dateValue.from;
215-
const endTime = dateValue.to;
215+
// Create new Date objects to avoid mutating the original dates
216+
const startTime = new Date(dateValue.from);
217+
const endTime = new Date(dateValue.to);
216218

217219
try {
218220
// Get first page
@@ -281,9 +283,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
281283
<TabPanel>
282284
<Grid numItems={2} className="gap-2 w-full mb-4">
283285
<Col>
284-
<Text>Select Time Range</Text>
285-
<DateRangePicker
286-
enableSelect={true}
286+
<UsageDatePicker
287287
value={dateValue}
288288
onValueChange={(value) => {
289289
setDateValue(value);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from "react";
2+
import { DateRangePicker, DateRangePickerValue, Text } from "@tremor/react";
3+
4+
interface UsageDatePickerProps {
5+
value: DateRangePickerValue;
6+
onValueChange: (value: DateRangePickerValue) => void;
7+
label?: string;
8+
className?: string;
9+
showTimeRange?: boolean;
10+
}
11+
12+
/**
13+
* Reusable date picker component for usage dashboards.
14+
* Handles proper time boundaries for date ranges, especially "Today" selections.
15+
* Addresses timezone issues by ensuring UTC time boundaries are set correctly.
16+
*/
17+
const UsageDatePicker: React.FC<UsageDatePickerProps> = ({
18+
value,
19+
onValueChange,
20+
label = "Select Time Range",
21+
className = "",
22+
showTimeRange = true
23+
}) => {
24+
const handleDateChange = (newValue: DateRangePickerValue) => {
25+
// Handle the case where "Today" or same-day selection is made
26+
if (newValue.from) {
27+
const adjustedValue = { ...newValue };
28+
29+
// Create new Date objects to avoid mutating the original dates
30+
const adjustedStartTime = new Date(newValue.from);
31+
let adjustedEndTime: Date;
32+
33+
if (newValue.to) {
34+
adjustedEndTime = new Date(newValue.to);
35+
} else {
36+
// If no end date is provided (like "Today" from dropdown), use the same date
37+
adjustedEndTime = new Date(newValue.from);
38+
}
39+
40+
// Check if it's the same day (Today selection or single day selection)
41+
const isSameDay =
42+
adjustedStartTime.toDateString() === adjustedEndTime.toDateString();
43+
44+
if (isSameDay) {
45+
// For same-day selections, set proper time boundaries
46+
// Use local timezone boundaries that will be converted to UTC properly
47+
adjustedStartTime.setHours(0, 0, 0, 0); // Start of day in local time
48+
adjustedEndTime.setHours(23, 59, 59, 999); // End of day in local time
49+
} else {
50+
// For multi-day ranges, set start to beginning of first day and end to end of last day
51+
adjustedStartTime.setHours(0, 0, 0, 0);
52+
adjustedEndTime.setHours(23, 59, 59, 999);
53+
}
54+
55+
adjustedValue.from = adjustedStartTime;
56+
adjustedValue.to = adjustedEndTime;
57+
58+
onValueChange(adjustedValue);
59+
} else {
60+
// If no from date, pass through as-is
61+
onValueChange(newValue);
62+
}
63+
};
64+
65+
const formatTimeRange = (from: Date | undefined, to: Date | undefined) => {
66+
if (!from || !to) return "";
67+
68+
const formatDateTime = (date: Date) => {
69+
return date.toLocaleString('en-US', {
70+
month: 'short',
71+
day: 'numeric',
72+
hour: '2-digit',
73+
minute: '2-digit',
74+
hour12: true,
75+
timeZoneName: 'short'
76+
});
77+
};
78+
79+
const isSameDay = from.toDateString() === to.toDateString();
80+
81+
if (isSameDay) {
82+
const dateStr = from.toLocaleDateString('en-US', {
83+
month: 'short',
84+
day: 'numeric',
85+
year: 'numeric'
86+
});
87+
const startTime = from.toLocaleTimeString('en-US', {
88+
hour: '2-digit',
89+
minute: '2-digit',
90+
hour12: true
91+
});
92+
const endTime = to.toLocaleTimeString('en-US', {
93+
hour: '2-digit',
94+
minute: '2-digit',
95+
hour12: true,
96+
timeZoneName: 'short'
97+
});
98+
return `${dateStr}: ${startTime} - ${endTime}`;
99+
} else {
100+
return `${formatDateTime(from)} - ${formatDateTime(to)}`;
101+
}
102+
};
103+
104+
return (
105+
<div className={className}>
106+
{label && <Text className="mb-2">{label}</Text>}
107+
<DateRangePicker
108+
enableSelect={true}
109+
value={value}
110+
onValueChange={handleDateChange}
111+
/>
112+
{showTimeRange && value.from && value.to && (
113+
<Text className="mt-1 text-xs text-gray-500">
114+
{formatTimeRange(value.from, value.to)}
115+
</Text>
116+
)}
117+
</div>
118+
);
119+
};
120+
121+
export default UsageDatePicker;

0 commit comments

Comments
 (0)