diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 251e51b7b90..d8661002aa1 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -6,7 +6,7 @@ * Works at 1m+ spend logs, by querying an aggregate table instead. */ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react" import { BarChart, Card, @@ -30,23 +30,17 @@ import { DateRangePicker, DateRangePickerValue, Button, -} from "@tremor/react"; -import UsageDatePicker from "./shared/usage_date_picker"; -import { AreaChart } from "@tremor/react"; +} from "@tremor/react" +import UsageDatePicker from "./shared/usage_date_picker" +import { AreaChart } from "@tremor/react" -import { userDailyActivityCall, tagListCall } from "./networking"; -import { Tag } from "./tag_management/types"; -import ViewUserSpend from "./view_user_spend"; -import TopKeyView from "./top_key_view"; -import { ActivityMetrics, processActivityData } from "./activity_metrics"; -import { - SpendMetrics, - DailyData, - ModelActivityData, - MetricWithMetadata, - KeyMetricWithMetadata, -} from "./usage/types"; -import EntityUsage from "./entity_usage"; +import { userDailyActivityCall, tagListCall } from "./networking" +import { Tag } from "./tag_management/types" +import ViewUserSpend from "./view_user_spend" +import TopKeyView from "./top_key_view" +import { ActivityMetrics, processActivityData } from "./activity_metrics" +import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from "./usage/types" +import EntityUsage from "./entity_usage" import { old_admin_roles, v2_admin_role_names, @@ -54,66 +48,69 @@ import { rolesAllowedToSeeUsage, rolesWithWriteAccess, internalUserRoles, -} from "../utils/roles"; -import { Team } from "./key_team_helpers/key_list"; -import { EntityList } from "./entity_usage"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { valueFormatterSpend } from "./usage/utils/value_formatters"; -import CloudZeroExportModal from "./cloudzero_export_modal"; +} from "../utils/roles" +import { Team } from "./key_team_helpers/key_list" +import { EntityList } from "./entity_usage" +import { formatNumberWithCommas } from "@/utils/dataUtils" +import { valueFormatterSpend } from "./usage/utils/value_formatters" +import CloudZeroExportModal from "./cloudzero_export_modal" +import { UiLoadingSpinner } from "./ui/ui-loading-spinner" interface NewUsagePageProps { - accessToken: string | null; - userRole: string | null; - userID: string | null; - teams: Team[]; - premiumUser: boolean; + accessToken: string | null + userRole: string | null + userID: string | null + teams: Team[] + premiumUser: boolean } -const NewUsagePage: React.FC = ({ - accessToken, - userRole, - userID, - teams, - premiumUser, -}) => { +const NewUsagePage: React.FC = ({ accessToken, userRole, userID, teams, premiumUser }) => { const [userSpendData, setUserSpendData] = useState<{ - results: DailyData[]; - metadata: any; - }>({ results: [], metadata: {} }); + results: DailyData[] + metadata: any + }>({ results: [], metadata: {} }) - // Add date range state + // Separate loading states for better UX + const [loading, setLoading] = useState(false) + const [isDateChanging, setIsDateChanging] = useState(false) + + // Create initial dates outside of state to prevent recreation + const initialFromDate = useMemo(() => new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), []) + const initialToDate = useMemo(() => new Date(), []) + + // Single date state that directly triggers data fetching const [dateValue, setDateValue] = useState({ - from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - to: new Date(), - }); + from: initialFromDate, + to: initialToDate, + }) - const [allTags, setAllTags] = useState([]); - const [modelViewType, setModelViewType] = useState<'groups' | 'individual'>('groups'); - const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false); + const [allTags, setAllTags] = useState([]) + const [modelViewType, setModelViewType] = useState<"groups" | "individual">("groups") + const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false) const getAllTags = async () => { if (!accessToken) { - return; + return } - const tags = await tagListCall(accessToken); + const tags = await tagListCall(accessToken) setAllTags( Object.values(tags).map((tag: Tag) => ({ label: tag.name, value: tag.name, - })) - ); - }; + })), + ) + } useEffect(() => { - getAllTags(); - }, [accessToken]); + getAllTags() + }, [accessToken]) // Derived states from userSpendData - const totalSpend = userSpendData.metadata?.total_spend || 0; + const totalSpend = userSpendData.metadata?.total_spend || 0 // Calculate top models from the breakdown data const getTopModels = () => { - const modelSpend: { [key: string]: MetricWithMetadata } = {}; + const modelSpend: { [key: string]: MetricWithMetadata } = {} userSpendData.results.forEach((day) => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { if (!modelSpend[model]) { @@ -130,26 +127,20 @@ const NewUsagePage: React.FC = ({ cache_creation_input_tokens: 0, }, metadata: {}, - api_key_breakdown: {} - }; + api_key_breakdown: {}, + } } - modelSpend[model].metrics.spend += metrics.metrics.spend; - modelSpend[model].metrics.prompt_tokens += - metrics.metrics.prompt_tokens; - modelSpend[model].metrics.completion_tokens += - metrics.metrics.completion_tokens; - modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens; - modelSpend[model].metrics.api_requests += metrics.metrics.api_requests; - modelSpend[model].metrics.successful_requests += - metrics.metrics.successful_requests || 0; - modelSpend[model].metrics.failed_requests += - metrics.metrics.failed_requests || 0; - modelSpend[model].metrics.cache_read_input_tokens += - metrics.metrics.cache_read_input_tokens || 0; - modelSpend[model].metrics.cache_creation_input_tokens += - metrics.metrics.cache_creation_input_tokens || 0; - }); - }); + modelSpend[model].metrics.spend += metrics.metrics.spend + modelSpend[model].metrics.prompt_tokens += metrics.metrics.prompt_tokens + modelSpend[model].metrics.completion_tokens += metrics.metrics.completion_tokens + modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens + modelSpend[model].metrics.api_requests += metrics.metrics.api_requests + modelSpend[model].metrics.successful_requests += metrics.metrics.successful_requests || 0 + modelSpend[model].metrics.failed_requests += metrics.metrics.failed_requests || 0 + modelSpend[model].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0 + modelSpend[model].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0 + }) + }) return Object.entries(modelSpend) .map(([model, metrics]) => ({ @@ -161,11 +152,11 @@ const NewUsagePage: React.FC = ({ tokens: metrics.metrics.total_tokens, })) .sort((a, b) => b.spend - a.spend) - .slice(0, 5); - }; + .slice(0, 5) + } const getTopModelGroups = () => { - const modelGroupSpend: { [key: string]: MetricWithMetadata } = {}; + const modelGroupSpend: { [key: string]: MetricWithMetadata } = {} userSpendData.results.forEach((day) => { Object.entries(day.breakdown.model_groups || {}).forEach(([modelGroup, metrics]) => { if (!modelGroupSpend[modelGroup]) { @@ -182,26 +173,21 @@ const NewUsagePage: React.FC = ({ cache_creation_input_tokens: 0, }, metadata: {}, - api_key_breakdown: {} - }; + api_key_breakdown: {}, + } } - modelGroupSpend[modelGroup].metrics.spend += metrics.metrics.spend; - modelGroupSpend[modelGroup].metrics.prompt_tokens += - metrics.metrics.prompt_tokens; - modelGroupSpend[modelGroup].metrics.completion_tokens += - metrics.metrics.completion_tokens; - modelGroupSpend[modelGroup].metrics.total_tokens += metrics.metrics.total_tokens; - modelGroupSpend[modelGroup].metrics.api_requests += metrics.metrics.api_requests; - modelGroupSpend[modelGroup].metrics.successful_requests += - metrics.metrics.successful_requests || 0; - modelGroupSpend[modelGroup].metrics.failed_requests += - metrics.metrics.failed_requests || 0; - modelGroupSpend[modelGroup].metrics.cache_read_input_tokens += - metrics.metrics.cache_read_input_tokens || 0; + modelGroupSpend[modelGroup].metrics.spend += metrics.metrics.spend + modelGroupSpend[modelGroup].metrics.prompt_tokens += metrics.metrics.prompt_tokens + modelGroupSpend[modelGroup].metrics.completion_tokens += metrics.metrics.completion_tokens + modelGroupSpend[modelGroup].metrics.total_tokens += metrics.metrics.total_tokens + modelGroupSpend[modelGroup].metrics.api_requests += metrics.metrics.api_requests + modelGroupSpend[modelGroup].metrics.successful_requests += metrics.metrics.successful_requests || 0 + modelGroupSpend[modelGroup].metrics.failed_requests += metrics.metrics.failed_requests || 0 + modelGroupSpend[modelGroup].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0 modelGroupSpend[modelGroup].metrics.cache_creation_input_tokens += - metrics.metrics.cache_creation_input_tokens || 0; - }); - }); + metrics.metrics.cache_creation_input_tokens || 0 + }) + }) return Object.entries(modelGroupSpend) .map(([modelGroup, metrics]) => ({ @@ -213,13 +199,13 @@ const NewUsagePage: React.FC = ({ tokens: metrics.metrics.total_tokens, })) .sort((a, b) => b.spend - a.spend) - .slice(0, 5); - }; + .slice(0, 5) + } // Calculate provider spend from the breakdown data const getProviderSpend = () => { - const providerSpend: { [key: string]: MetricWithMetadata } = {}; - userSpendData.results.forEach(day => { + const providerSpend: { [key: string]: MetricWithMetadata } = {} + userSpendData.results.forEach((day) => { Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { if (!providerSpend[provider]) { providerSpend[provider] = { @@ -232,31 +218,23 @@ const NewUsagePage: React.FC = ({ successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }, metadata: {}, - api_key_breakdown: {} - }; + api_key_breakdown: {}, } - providerSpend[provider].metrics.spend += metrics.metrics.spend; - providerSpend[provider].metrics.prompt_tokens += - metrics.metrics.prompt_tokens; - providerSpend[provider].metrics.completion_tokens += - metrics.metrics.completion_tokens; - providerSpend[provider].metrics.total_tokens += - metrics.metrics.total_tokens; - providerSpend[provider].metrics.api_requests += - metrics.metrics.api_requests; - providerSpend[provider].metrics.successful_requests += - metrics.metrics.successful_requests || 0; - providerSpend[provider].metrics.failed_requests += - metrics.metrics.failed_requests || 0; - providerSpend[provider].metrics.cache_read_input_tokens += - metrics.metrics.cache_read_input_tokens || 0; - providerSpend[provider].metrics.cache_creation_input_tokens += - metrics.metrics.cache_creation_input_tokens || 0; - }); - }); + } + providerSpend[provider].metrics.spend += metrics.metrics.spend + providerSpend[provider].metrics.prompt_tokens += metrics.metrics.prompt_tokens + providerSpend[provider].metrics.completion_tokens += metrics.metrics.completion_tokens + providerSpend[provider].metrics.total_tokens += metrics.metrics.total_tokens + providerSpend[provider].metrics.api_requests += metrics.metrics.api_requests + providerSpend[provider].metrics.successful_requests += metrics.metrics.successful_requests || 0 + providerSpend[provider].metrics.failed_requests += metrics.metrics.failed_requests || 0 + providerSpend[provider].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0 + providerSpend[provider].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0 + }) + }) return Object.entries(providerSpend).map(([provider, metrics]) => ({ provider, @@ -265,12 +243,12 @@ const NewUsagePage: React.FC = ({ successful_requests: metrics.metrics.successful_requests, failed_requests: metrics.metrics.failed_requests, tokens: metrics.metrics.total_tokens, - })); - }; + })) + } // Calculate top API keys from the breakdown data const getTopKeys = () => { - const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; + const keySpend: { [key: string]: KeyMetricWithMetadata } = {} userSpendData.results.forEach((day) => { Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { if (!keySpend[key]) { @@ -288,26 +266,21 @@ const NewUsagePage: React.FC = ({ }, metadata: { key_alias: metrics.metadata.key_alias, - team_id: null - } - }; + team_id: null, + }, + } } - keySpend[key].metrics.spend += metrics.metrics.spend; - keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - keySpend[key].metrics.completion_tokens += - metrics.metrics.completion_tokens; - keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens; - keySpend[key].metrics.api_requests += metrics.metrics.api_requests; - keySpend[key].metrics.successful_requests += - metrics.metrics.successful_requests; - keySpend[key].metrics.failed_requests += - metrics.metrics.failed_requests; - keySpend[key].metrics.cache_read_input_tokens += - metrics.metrics.cache_read_input_tokens || 0; - keySpend[key].metrics.cache_creation_input_tokens += - metrics.metrics.cache_creation_input_tokens || 0; - }); - }); + keySpend[key].metrics.spend += metrics.metrics.spend + keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens + keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens + keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens + keySpend[key].metrics.api_requests += metrics.metrics.api_requests + keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests + keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests + keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0 + keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0 + }) + }) return Object.entries(keySpend) .map(([api_key, metrics]) => ({ @@ -316,58 +289,41 @@ const NewUsagePage: React.FC = ({ spend: metrics.metrics.spend, })) .sort((a, b) => b.spend - a.spend) - .slice(0, 5); - }; + .slice(0, 5) + } + + const fetchUserSpendData = useCallback(async () => { + if (!accessToken || !dateValue.from || !dateValue.to) return + + setLoading(true) - const fetchUserSpendData = async () => { - if (!accessToken || !dateValue.from || !dateValue.to) return; // Create new Date objects to avoid mutating the original dates - const startTime = new Date(dateValue.from); - const endTime = new Date(dateValue.to); + const startTime = new Date(dateValue.from) + const endTime = new Date(dateValue.to) try { // Get first page - const firstPageData = await userDailyActivityCall( - accessToken, - startTime, - endTime - ); - - // Check if we need to fetch more pages - if (firstPageData.metadata.total_pages > 10) { - throw new Error( - "Too many pages of data (>10). Please select a smaller date range." - ); - } + const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime) // If only one page, just set the data if (firstPageData.metadata.total_pages <= 1) { - setUserSpendData(firstPageData); - return; + setUserSpendData(firstPageData) + return } // Fetch all pages - const allResults = [...firstPageData.results]; - const aggregatedMetadata = { ...firstPageData.metadata }; + const allResults = [...firstPageData.results] + const aggregatedMetadata = { ...firstPageData.metadata } for (let page = 2; page <= firstPageData.metadata.total_pages; page++) { - const pageData = await userDailyActivityCall( - accessToken, - startTime, - endTime, - page - ); - allResults.push(...pageData.results); + const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page) + allResults.push(...pageData.results) if (pageData.metadata) { - aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0; - aggregatedMetadata.total_api_requests += - pageData.metadata.total_api_requests || 0; - aggregatedMetadata.total_successful_requests += - pageData.metadata.total_successful_requests || 0; - aggregatedMetadata.total_failed_requests += - pageData.metadata.total_failed_requests || 0; - aggregatedMetadata.total_tokens += - pageData.metadata.total_tokens || 0; + aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0 + aggregatedMetadata.total_api_requests += pageData.metadata.total_api_requests || 0 + aggregatedMetadata.total_successful_requests += pageData.metadata.total_successful_requests || 0 + aggregatedMetadata.total_failed_requests += pageData.metadata.total_failed_requests || 0 + aggregatedMetadata.total_tokens += pageData.metadata.total_tokens || 0 } } @@ -375,20 +331,56 @@ const NewUsagePage: React.FC = ({ setUserSpendData({ results: allResults, metadata: aggregatedMetadata, - }); + }) } catch (error) { - console.error("Error fetching user spend data:", error); - throw error; + console.error("Error fetching user spend data:", error) + } finally { + setLoading(false) + setIsDateChanging(false) } - }; + }, [accessToken, dateValue.from, dateValue.to]) + // Super responsive date change handler + const handleDateChange = useCallback((newValue: DateRangePickerValue) => { + // Instant visual feedback + setIsDateChanging(true) + setLoading(true) + + // Update date immediately for UI responsiveness + setDateValue(newValue) + }, []) + + // Debounced effect for data fetching with shorter delay useEffect(() => { - fetchUserSpendData(); - }, [accessToken, dateValue]); + if (!dateValue.from || !dateValue.to) return - const modelMetrics = processActivityData(userSpendData, "models"); - const keyMetrics = processActivityData(userSpendData, "api_keys"); - const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers"); + const timeoutId = setTimeout(() => { + fetchUserSpendData() + }, 50) // Very short debounce + + return () => clearTimeout(timeoutId) + }, [fetchUserSpendData]) + + // Enhanced loading component with better visual feedback + const ChartLoader = () => ( +
+
+ +
+ + {isDateChanging ? "Processing date selection..." : "Loading chart data..."} + + + {isDateChanging ? "This will only take a moment" : "Fetching your data"} + +
+
+
+ ) + + const modelMetrics = processActivityData(userSpendData, "models") + const keyMetrics = processActivityData(userSpendData, "api_keys") + const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers") return (
@@ -432,29 +424,16 @@ const NewUsagePage: React.FC = ({ - {all_admin_roles.includes(userRole || "") ? ( - Global Usage - ) : ( - Your Usage - )} + {all_admin_roles.includes(userRole || "") ? Global Usage : Your Usage} Team Usage - {all_admin_roles.includes(userRole || "") ? ( - Tag Usage - ) : ( - <> - )} + {all_admin_roles.includes(userRole || "") ? Tag Usage : <>} {/* Your Usage Panel */} - { - setDateValue(value); - }} - /> + @@ -475,12 +454,7 @@ const NewUsagePage: React.FC = ({ {new Date().toLocaleString("default", { month: "long", })}{" "} - 1 -{" "} - {new Date( - new Date().getFullYear(), - new Date().getMonth() + 1, - 0 - ).getDate()} + 1 - {new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate()} = ({ Total Requests - {userSpendData.metadata?.total_api_requests?.toLocaleString() || - 0} + {userSpendData.metadata?.total_api_requests?.toLocaleString() || 0} Successful Requests - {userSpendData.metadata?.total_successful_requests?.toLocaleString() || - 0} + {userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0} Failed Requests - {userSpendData.metadata?.total_failed_requests?.toLocaleString() || - 0} + {userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0} Total Tokens - {userSpendData.metadata?.total_tokens?.toLocaleString() || - 0} + {userSpendData.metadata?.total_tokens?.toLocaleString() || 0} @@ -530,10 +500,8 @@ const NewUsagePage: React.FC = ({ $ {formatNumberWithCommas( - (totalSpend || 0) / - (userSpendData.metadata?.total_api_requests || - 1), - 4 + (totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1), + 4, )} @@ -545,47 +513,37 @@ const NewUsagePage: React.FC = ({ Daily Spend - - new Date(a.date).getTime() - - new Date(b.date).getTime() - )} - index="date" - categories={["metrics.spend"]} - colors={["cyan"]} - valueFormatter={valueFormatterSpend} - yAxisWidth={100} - showLegend={false} - customTooltip={({ payload, active }) => { - if (!active || !payload?.[0]) return null; - const data = payload[0].payload; - return ( -
-

{data.date}

-

- Spend: $ - {formatNumberWithCommas( - data.metrics.spend, - 2 - )} -

-

- Requests: {data.metrics.api_requests} -

-

- Successful: {data.metrics.successful_requests} -

-

- Failed: {data.metrics.failed_requests} -

-

- Tokens: {data.metrics.total_tokens} -

-
- ); - }} - /> + {loading ? ( + + ) : ( + new Date(a.date).getTime() - new Date(b.date).getTime(), + )} + index="date" + categories={["metrics.spend"]} + colors={["cyan"]} + valueFormatter={valueFormatterSpend} + yAxisWidth={100} + showLegend={false} + customTooltip={({ payload, active }) => { + if (!active || !payload?.[0]) return null + const data = payload[0].payload + return ( +
+

{data.date}

+

+ Spend: ${formatNumberWithCommas(data.metrics.spend, 2)} +

+

Requests: {data.metrics.api_requests}

+

Successful: {data.metrics.successful_requests}

+

Failed: {data.metrics.failed_requests}

+

Tokens: {data.metrics.total_tokens}

+
+ ) + }} + /> + )}
{/* Top API Keys */} @@ -607,71 +565,61 @@ const NewUsagePage: React.FC = ({
- - {modelViewType === 'groups' ? 'Top Public Model Names' : 'Top Litellm Models'} - + {modelViewType === "groups" ? "Top Public Model Names" : "Top Litellm Models"}
- { - if (!active || !payload?.[0]) return null; - const data = payload[0].payload; - return ( -
-

{data.key}

-

- Spend: $ - {formatNumberWithCommas(data.spend, 2)} -

-

- Total Requests:{" "} - {data.requests.toLocaleString()} -

-

- Successful:{" "} - {data.successful_requests.toLocaleString()} -

-

- Failed:{" "} - {data.failed_requests.toLocaleString()} -

-

- Tokens: {data.tokens.toLocaleString()} -

-
- ); - }} - /> + {loading ? ( + + ) : ( + { + if (!active || !payload?.[0]) return null + const data = payload[0].payload + return ( +
+

{data.key}

+

Spend: ${formatNumberWithCommas(data.spend, 2)}

+

Total Requests: {data.requests.toLocaleString()}

+

+ Successful: {data.successful_requests.toLocaleString()} +

+

Failed: {data.failed_requests.toLocaleString()}

+

Tokens: {data.tokens.toLocaleString()}

+
+ ) + }} + /> + )}
@@ -681,62 +629,52 @@ const NewUsagePage: React.FC = ({
Spend by Provider
- - - - `$${formatNumberWithCommas(value, 2)}` - } - colors={["cyan"]} - /> - - - - - - Provider - Spend - - Successful - - - Failed - - Tokens - - - - {getProviderSpend() - .filter((provider) => provider.spend > 0) - .map((provider) => ( - - {provider.provider} - - $ - {formatNumberWithCommas( - provider.spend, - 2 - )} - - - {provider.successful_requests.toLocaleString()} - - - {provider.failed_requests.toLocaleString()} - - - {provider.tokens.toLocaleString()} - - - ))} - -
- -
+ {loading ? ( + + ) : ( + + + `$${formatNumberWithCommas(value, 2)}`} + colors={["cyan"]} + /> + + + + + + Provider + Spend + Successful + Failed + Tokens + + + + {getProviderSpend() + .filter((provider) => provider.spend > 0) + .map((provider) => ( + + {provider.provider} + ${formatNumberWithCommas(provider.spend, 2)} + + {provider.successful_requests.toLocaleString()} + + + {provider.failed_requests.toLocaleString()} + + {provider.tokens.toLocaleString()} + + ))} + +
+ +
+ )} @@ -796,25 +734,22 @@ const NewUsagePage: React.FC = ({ accessToken={accessToken} />
- ); -}; + ) +} // Add this helper function to process model-specific activity data -const getModelActivityData = (userSpendData: { - results: DailyData[]; - metadata: any; -}) => { +const getModelActivityData = (userSpendData: { results: DailyData[]; metadata: any }) => { const modelData: { [key: string]: { - total_requests: number; - total_tokens: number; + total_requests: number + total_tokens: number daily_data: Array<{ - date: string; - api_requests: number; - total_tokens: number; - }>; - }; - } = {}; + date: string + api_requests: number + total_tokens: number + }> + } + } = {} userSpendData.results.forEach((day: DailyData) => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { @@ -823,20 +758,20 @@ const getModelActivityData = (userSpendData: { total_requests: 0, total_tokens: 0, daily_data: [], - }; + } } - modelData[model].total_requests += metrics.metrics.api_requests; - modelData[model].total_tokens += metrics.metrics.total_tokens; + modelData[model].total_requests += metrics.metrics.api_requests + modelData[model].total_tokens += metrics.metrics.total_tokens modelData[model].daily_data.push({ date: day.date, api_requests: metrics.metrics.api_requests, total_tokens: metrics.metrics.total_tokens, - }); - }); - }); + }) + }) + }) - return modelData; -}; + return modelData +} -export default NewUsagePage; +export default NewUsagePage diff --git a/ui/litellm-dashboard/src/components/shared/usage_date_picker.tsx b/ui/litellm-dashboard/src/components/shared/usage_date_picker.tsx index 2d9bba75d56..d1c81ce9eaa 100644 --- a/ui/litellm-dashboard/src/components/shared/usage_date_picker.tsx +++ b/ui/litellm-dashboard/src/components/shared/usage_date_picker.tsx @@ -1,121 +1,156 @@ -import React from "react"; -import { DateRangePicker, DateRangePickerValue, Text } from "@tremor/react"; +import React, { useCallback, useState, useRef } from "react" +import { DateRangePicker, DateRangePickerValue, Text } from "@tremor/react" interface UsageDatePickerProps { - value: DateRangePickerValue; - onValueChange: (value: DateRangePickerValue) => void; - label?: string; - className?: string; - showTimeRange?: boolean; + value: DateRangePickerValue + onValueChange: (value: DateRangePickerValue) => void + label?: string + className?: string + showTimeRange?: boolean } /** - * Reusable date picker component for usage dashboards. - * Handles proper time boundaries for date ranges, especially "Today" selections. - * Addresses timezone issues by ensuring UTC time boundaries are set correctly. + * Ultra responsive date picker with instant click feedback */ const UsageDatePicker: React.FC = ({ value, onValueChange, label = "Select Time Range", className = "", - showTimeRange = true + showTimeRange = true, }) => { - const handleDateChange = (newValue: DateRangePickerValue) => { - // Handle the case where "Today" or same-day selection is made - if (newValue.from) { - const adjustedValue = { ...newValue }; - - // Create new Date objects to avoid mutating the original dates - const adjustedStartTime = new Date(newValue.from); - let adjustedEndTime: Date; - - if (newValue.to) { - adjustedEndTime = new Date(newValue.to); - } else { - // If no end date is provided (like "Today" from dropdown), use the same date - adjustedEndTime = new Date(newValue.from); - } - - // Check if it's the same day (Today selection or single day selection) - const isSameDay = - adjustedStartTime.toDateString() === adjustedEndTime.toDateString(); - - if (isSameDay) { - // For same-day selections, set proper time boundaries - // Use local timezone boundaries that will be converted to UTC properly - adjustedStartTime.setHours(0, 0, 0, 0); // Start of day in local time - adjustedEndTime.setHours(23, 59, 59, 999); // End of day in local time - } else { - // For multi-day ranges, set start to beginning of first day and end to end of last day - adjustedStartTime.setHours(0, 0, 0, 0); - adjustedEndTime.setHours(23, 59, 59, 999); - } - - adjustedValue.from = adjustedStartTime; - adjustedValue.to = adjustedEndTime; - - onValueChange(adjustedValue); - } else { - // If no from date, pass through as-is - onValueChange(newValue); - } - }; + const [showSelectedFeedback, setShowSelectedFeedback] = useState(false) + const datePickerRef = useRef(null) + + // This only triggers AFTER user has actually made a selection + const handleDateChange = useCallback( + (newValue: DateRangePickerValue) => { + // Show "Selected" feedback ONLY after actual selection is made + setShowSelectedFeedback(true) + + // Hide the feedback after a short time + setTimeout(() => setShowSelectedFeedback(false), 1500) + + // Update parent immediately + onValueChange(newValue) + + // Do heavy processing in background + requestIdleCallback( + () => { + if (newValue.from) { + 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 + onValueChange(adjustedValue) + } + }, + { timeout: 100 }, + ) + }, + [onValueChange], + ) + + const formatTimeRange = useCallback((from: Date | undefined, to: Date | undefined) => { + if (!from || !to) return "" - const formatTimeRange = (from: Date | undefined, to: Date | undefined) => { - if (!from || !to) return ""; - const formatDateTime = (date: Date) => { - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', + return date.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", hour12: true, - timeZoneName: 'short' - }); - }; - - const isSameDay = from.toDateString() === to.toDateString(); - - if (isSameDay) { - const dateStr = from.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - const startTime = from.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - const endTime = to.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true, - timeZoneName: 'short' - }); - return `${dateStr}: ${startTime} - ${endTime}`; - } else { - return `${formatDateTime(from)} - ${formatDateTime(to)}`; + timeZoneName: "short", + }) } - }; + + const isSameDay = from.toDateString() === to.toDateString() + + if (isSameDay) { + const dateStr = from.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + const startTime = from.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }) + const endTime = to.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: true, + timeZoneName: "short", + }) + return `${dateStr}: ${startTime} - ${endTime}` + } else { + return `${formatDateTime(from)} - ${formatDateTime(to)}` + } + }, []) return (
{label && {label}} - + + {/* Container with relative positioning for absolute placement */} +
+
+ +
+ + {/* ONLY SHOW AFTER ACTUAL SELECTION IS COMPLETED */} + {showSelectedFeedback && ( +
+
+
+ ✓ +
+ Selected +
+
+ )} +
+ + {/* Time range display */} {showTimeRange && value.from && value.to && ( - - {formatTimeRange(value.from, value.to)} - + {formatTimeRange(value.from, value.to)} )}
- ); -}; + ) +} -export default UsageDatePicker; \ No newline at end of file +export default UsageDatePicker