mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
c47f77a348
commit
0da56beb79
4 changed files with 98 additions and 12 deletions
|
|
@ -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 } = {};
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue