feat: adding a timezone picker to the usage page, to be able to view by timezone, backend already supports this just ui change

This commit is contained in:
Ryan Crabbe 2026-03-09 11:53:07 -07:00
parent c47f77a348
commit 0da56beb79
4 changed files with 98 additions and 12 deletions

View file

@ -85,9 +85,10 @@ interface EntityUsageProps {
entityList: EntityList[] | null;
premiumUser: boolean;
dateValue: DateRangePickerValue;
timezoneOffset?: number;
}
const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, entityId, entityList, dateValue }) => {
const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, entityId, entityList, dateValue, timezoneOffset }) => {
const [spendData, setSpendData] = useState<EntitySpendData>({
results: [],
metadata: {
@ -119,6 +120,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ accessToken, entityType, enti
endTime,
1,
selectedTags.length > 0 ? selectedTags[0] : null,
timezoneOffset,
);
setSpendData(data);
} else {
@ -173,7 +180,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
useEffect(() => {
fetchSpendData();
}, [accessToken, dateValue, entityId, selectedTags]);
}, [accessToken, dateValue, entityId, selectedTags, timezoneOffset]);
const getTopModels = () => {
const modelSpend: { [key: string]: any } = {};

View file

@ -78,6 +78,8 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
to: initialToDate,
});
const [timezoneOffset, setTimezoneOffset] = useState<number>(() => new Date().getTimezoneOffset());
const [allTags, setAllTags] = useState<EntityList[]>([]);
const { data: customers = [] } = useCustomers();
const { data: agentsResponse } = useAgents();
@ -378,14 +380,14 @@ const UsagePage: React.FC<UsagePageProps> = ({ 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<UsagePageProps> = ({ 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<UsagePageProps> = ({ 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<UsagePageProps> = ({ teams, organizations }) => {
onChange={(value) => setUsageView(value)}
isAdmin={isAdmin}
/>
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} timezoneOffset={timezoneOffset} onTimezoneChange={setTimezoneOffset} />
</div>
{/* Your Usage Panel */}
{usageView === "global" && (
@ -825,6 +827,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
})) || null
}
premiumUser={premiumUser}
timezoneOffset={timezoneOffset}
/>
)}
@ -843,6 +846,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}
premiumUser={premiumUser}
dateValue={dateValue}
timezoneOffset={timezoneOffset}
/>
)}
@ -861,6 +865,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}
premiumUser={premiumUser}
dateValue={dateValue}
timezoneOffset={timezoneOffset}
/>
)}
{/* Tag Usage Panel */}
@ -891,6 +896,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
entityList={allTags}
premiumUser={premiumUser}
dateValue={dateValue}
timezoneOffset={timezoneOffset}
/>
</>
)}
@ -905,6 +911,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}
premiumUser={premiumUser}
dateValue={dateValue}
timezoneOffset={timezoneOffset}
/>
)}
{/* User Usage Panel */}
@ -917,6 +924,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
entityList={userOptions.length > 0 ? userOptions : null}
premiumUser={premiumUser}
dateValue={dateValue}
timezoneOffset={timezoneOffset}
/>
)}
{/* User Agent Activity Panel */}

View file

@ -1690,6 +1690,7 @@ const buildDailyActivityUrl = (
endTime: Date,
page: number,
extraQueryParams?: Record<string, DailyActivityQueryValue>,
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<string, DailyActivityQueryValue>;
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);
}

View file

@ -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<AdvancedDatePickerProps> = ({
onValueChange,
label = "Select Time Range",
showTimeRange = true,
timezoneOffset,
onTimezoneChange,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [tempValue, setTempValue] = useState<DateRangePickerValue>(value);
@ -420,6 +460,21 @@ const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
</div>
)}
</div>
{/* Timezone selector */}
{onTimezoneChange && (
<select
value={timezoneOffset !== undefined ? timezoneOffset : getLocalTimezoneOffset()}
onChange={(e) => onTimezoneChange(Number(e.target.value))}
className="px-2 py-2 text-xs 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 text-gray-700"
>
<option value={getLocalTimezoneOffset()}>Local ({formatTimezoneLabel(getLocalTimezoneOffset())})</option>
{TIMEZONE_OPTIONS.map((tz) => (
<option key={tz.label} value={tz.offset}>
{tz.label}
</option>
))}
</select>
)}
</div>
);
};