mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
After selecting date range show loader on usage cost charts (#13113)
* prettier * added loader in bar chart * prettier * added existing loader style * make datepicker responsive
This commit is contained in:
parent
d10a5a69af
commit
c4088841d9
2 changed files with 460 additions and 490 deletions
|
|
@ -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<NewUsagePageProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
userID,
|
||||
teams,
|
||||
premiumUser,
|
||||
}) => {
|
||||
const NewUsagePage: React.FC<NewUsagePageProps> = ({ 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<DateRangePickerValue>({
|
||||
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
to: new Date(),
|
||||
});
|
||||
from: initialFromDate,
|
||||
to: initialToDate,
|
||||
})
|
||||
|
||||
const [allTags, setAllTags] = useState<EntityList[]>([]);
|
||||
const [modelViewType, setModelViewType] = useState<'groups' | 'individual'>('groups');
|
||||
const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false);
|
||||
const [allTags, setAllTags] = useState<EntityList[]>([])
|
||||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
},
|
||||
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<NewUsagePageProps> = ({
|
|||
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<NewUsagePageProps> = ({
|
|||
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 = () => (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<UiLoadingSpinner className="size-5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs mt-1">
|
||||
{isDateChanging ? "This will only take a moment" : "Fetching your data"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const modelMetrics = processActivityData(userSpendData, "models")
|
||||
const keyMetrics = processActivityData(userSpendData, "api_keys")
|
||||
const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers")
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }} className="p-8 relative">
|
||||
|
|
@ -432,29 +424,16 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
|
||||
<TabGroup>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
{all_admin_roles.includes(userRole || "") ? (
|
||||
<Tab>Global Usage</Tab>
|
||||
) : (
|
||||
<Tab>Your Usage</Tab>
|
||||
)}
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>Global Usage</Tab> : <Tab>Your Usage</Tab>}
|
||||
<Tab>Team Usage</Tab>
|
||||
{all_admin_roles.includes(userRole || "") ? (
|
||||
<Tab>Tag Usage</Tab>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{/* Your Usage Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={2} className="gap-2 w-full mb-4">
|
||||
<Col>
|
||||
<UsageDatePicker
|
||||
value={dateValue}
|
||||
onValueChange={(value) => {
|
||||
setDateValue(value);
|
||||
}}
|
||||
/>
|
||||
<UsageDatePicker value={dateValue} onValueChange={handleDateChange} />
|
||||
</Col>
|
||||
</Grid>
|
||||
<TabGroup>
|
||||
|
|
@ -475,12 +454,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
{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()}
|
||||
</Text>
|
||||
|
||||
<ViewUserSpend
|
||||
|
|
@ -500,29 +474,25 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<Card>
|
||||
<Title>Total Requests</Title>
|
||||
<Text className="text-2xl font-bold mt-2">
|
||||
{userSpendData.metadata?.total_api_requests?.toLocaleString() ||
|
||||
0}
|
||||
{userSpendData.metadata?.total_api_requests?.toLocaleString() || 0}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Title>Successful Requests</Title>
|
||||
<Text className="text-2xl font-bold mt-2 text-green-600">
|
||||
{userSpendData.metadata?.total_successful_requests?.toLocaleString() ||
|
||||
0}
|
||||
{userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Title>Failed Requests</Title>
|
||||
<Text className="text-2xl font-bold mt-2 text-red-600">
|
||||
{userSpendData.metadata?.total_failed_requests?.toLocaleString() ||
|
||||
0}
|
||||
{userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Title>Total Tokens</Title>
|
||||
<Text className="text-2xl font-bold mt-2">
|
||||
{userSpendData.metadata?.total_tokens?.toLocaleString() ||
|
||||
0}
|
||||
{userSpendData.metadata?.total_tokens?.toLocaleString() || 0}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
|
|
@ -530,10 +500,8 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<Text className="text-2xl font-bold mt-2">
|
||||
$
|
||||
{formatNumberWithCommas(
|
||||
(totalSpend || 0) /
|
||||
(userSpendData.metadata?.total_api_requests ||
|
||||
1),
|
||||
4
|
||||
(totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1),
|
||||
4,
|
||||
)}
|
||||
</Text>
|
||||
</Card>
|
||||
|
|
@ -545,47 +513,37 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<Col numColSpan={2}>
|
||||
<Card>
|
||||
<Title>Daily Spend</Title>
|
||||
<BarChart
|
||||
data={[...userSpendData.results].sort(
|
||||
(a, b) =>
|
||||
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 (
|
||||
<div className="bg-white p-4 shadow-lg rounded-lg border">
|
||||
<p className="font-bold">{data.date}</p>
|
||||
<p className="text-cyan-500">
|
||||
Spend: $
|
||||
{formatNumberWithCommas(
|
||||
data.metrics.spend,
|
||||
2
|
||||
)}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Requests: {data.metrics.api_requests}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Successful: {data.metrics.successful_requests}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Failed: {data.metrics.failed_requests}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Tokens: {data.metrics.total_tokens}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
) : (
|
||||
<BarChart
|
||||
data={[...userSpendData.results].sort(
|
||||
(a, b) => 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 (
|
||||
<div className="bg-white p-4 shadow-lg rounded-lg border">
|
||||
<p className="font-bold">{data.date}</p>
|
||||
<p className="text-cyan-500">
|
||||
Spend: ${formatNumberWithCommas(data.metrics.spend, 2)}
|
||||
</p>
|
||||
<p className="text-gray-600">Requests: {data.metrics.api_requests}</p>
|
||||
<p className="text-gray-600">Successful: {data.metrics.successful_requests}</p>
|
||||
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
|
||||
<p className="text-gray-600">Tokens: {data.metrics.total_tokens}</p>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Top API Keys */}
|
||||
|
|
@ -607,71 +565,61 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<Col numColSpan={1}>
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>
|
||||
{modelViewType === 'groups' ? 'Top Public Model Names' : 'Top Litellm Models'}
|
||||
</Title>
|
||||
<Title>{modelViewType === "groups" ? "Top Public Model Names" : "Top Litellm Models"}</Title>
|
||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
||||
modelViewType === 'groups'
|
||||
? 'bg-white shadow-sm text-gray-900'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
modelViewType === "groups"
|
||||
? "bg-white shadow-sm text-gray-900"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
onClick={() => setModelViewType('groups')}
|
||||
onClick={() => setModelViewType("groups")}
|
||||
>
|
||||
Public Model Name
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
||||
modelViewType === 'individual'
|
||||
? 'bg-white shadow-sm text-gray-900'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
modelViewType === "individual"
|
||||
? "bg-white shadow-sm text-gray-900"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
onClick={() => setModelViewType('individual')}
|
||||
onClick={() => setModelViewType("individual")}
|
||||
>
|
||||
Litellm Model Name
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<BarChart
|
||||
className="mt-4 h-40"
|
||||
data={modelViewType === 'groups' ? getTopModelGroups() : getTopModels()}
|
||||
index="key"
|
||||
categories={["spend"]}
|
||||
colors={["cyan"]}
|
||||
valueFormatter={valueFormatterSpend}
|
||||
layout="vertical"
|
||||
yAxisWidth={200}
|
||||
showLegend={false}
|
||||
customTooltip={({ payload, active }) => {
|
||||
if (!active || !payload?.[0]) return null;
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-white p-4 shadow-lg rounded-lg border">
|
||||
<p className="font-bold">{data.key}</p>
|
||||
<p className="text-cyan-500">
|
||||
Spend: $
|
||||
{formatNumberWithCommas(data.spend, 2)}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Total Requests:{" "}
|
||||
{data.requests.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-green-600">
|
||||
Successful:{" "}
|
||||
{data.successful_requests.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-red-600">
|
||||
Failed:{" "}
|
||||
{data.failed_requests.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
Tokens: {data.tokens.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
) : (
|
||||
<BarChart
|
||||
className="mt-4 h-40"
|
||||
data={modelViewType === "groups" ? getTopModelGroups() : getTopModels()}
|
||||
index="key"
|
||||
categories={["spend"]}
|
||||
colors={["cyan"]}
|
||||
valueFormatter={valueFormatterSpend}
|
||||
layout="vertical"
|
||||
yAxisWidth={200}
|
||||
showLegend={false}
|
||||
customTooltip={({ payload, active }) => {
|
||||
if (!active || !payload?.[0]) return null
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white p-4 shadow-lg rounded-lg border">
|
||||
<p className="font-bold">{data.key}</p>
|
||||
<p className="text-cyan-500">Spend: ${formatNumberWithCommas(data.spend, 2)}</p>
|
||||
<p className="text-gray-600">Total Requests: {data.requests.toLocaleString()}</p>
|
||||
<p className="text-green-600">
|
||||
Successful: {data.successful_requests.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-red-600">Failed: {data.failed_requests.toLocaleString()}</p>
|
||||
<p className="text-gray-600">Tokens: {data.tokens.toLocaleString()}</p>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
|
|
@ -681,62 +629,52 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Spend by Provider</Title>
|
||||
</div>
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
<DonutChart
|
||||
className="mt-4 h-40"
|
||||
data={getProviderSpend()}
|
||||
index="provider"
|
||||
category="spend"
|
||||
valueFormatter={(value) =>
|
||||
`$${formatNumberWithCommas(value, 2)}`
|
||||
}
|
||||
colors={["cyan"]}
|
||||
/>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">
|
||||
Successful
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">
|
||||
Failed
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{getProviderSpend()
|
||||
.filter((provider) => provider.spend > 0)
|
||||
.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>{provider.provider}</TableCell>
|
||||
<TableCell>
|
||||
$
|
||||
{formatNumberWithCommas(
|
||||
provider.spend,
|
||||
2
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-red-600">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{provider.tokens.toLocaleString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Grid>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
) : (
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
<DonutChart
|
||||
className="mt-4 h-40"
|
||||
data={getProviderSpend()}
|
||||
index="provider"
|
||||
category="spend"
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
colors={["cyan"]}
|
||||
/>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{getProviderSpend()
|
||||
.filter((provider) => provider.spend > 0)
|
||||
.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>{provider.provider}</TableCell>
|
||||
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-red-600">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Grid>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
|
|
@ -796,25 +734,22 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
accessToken={accessToken}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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<UsageDatePickerProps> = ({
|
||||
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<HTMLDivElement>(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 (
|
||||
<div className={className}>
|
||||
{label && <Text className="mb-2">{label}</Text>}
|
||||
<DateRangePicker
|
||||
enableSelect={true}
|
||||
value={value}
|
||||
onValueChange={handleDateChange}
|
||||
/>
|
||||
|
||||
{/* Container with relative positioning for absolute placement */}
|
||||
<div className="relative w-fit">
|
||||
<div ref={datePickerRef}>
|
||||
<DateRangePicker
|
||||
enableSelect={true}
|
||||
value={value}
|
||||
onValueChange={handleDateChange} // Only triggers on actual selection
|
||||
placeholder="Select date range"
|
||||
enableClear={false}
|
||||
style={{ zIndex: 100 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ONLY SHOW AFTER ACTUAL SELECTION IS COMPLETED */}
|
||||
{showSelectedFeedback && (
|
||||
<div
|
||||
className="absolute top-1/2 animate-pulse"
|
||||
style={{
|
||||
left: "calc(100% + 8px)",
|
||||
transform: "translateY(-50%)",
|
||||
zIndex: 110,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-green-600 text-sm font-medium bg-white px-2 py-1 rounded-full border border-green-200 shadow-sm whitespace-nowrap">
|
||||
<div className="w-3 h-3 bg-green-500 text-white rounded-full flex items-center justify-center text-xs">
|
||||
✓
|
||||
</div>
|
||||
<span className="text-xs">Selected</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time range display */}
|
||||
{showTimeRange && value.from && value.to && (
|
||||
<Text className="mt-1 text-xs text-gray-500">
|
||||
{formatTimeRange(value.from, value.to)}
|
||||
</Text>
|
||||
<Text className="mt-2 text-xs text-gray-500">{formatTimeRange(value.from, value.to)}</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default UsageDatePicker;
|
||||
export default UsageDatePicker
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue