diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index bb3402f260..499209ad90 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1654,6 +1654,10 @@ async def ui_view_spend_logs( # noqa: PLR0915 default=50, description="Number of items per page", ge=1, le=100 ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + status_filter: Optional[str] = fastapi.Query( + default=None, + description="Filter logs by status (e.g., success, failure)" + ), ): """ View spend logs for UI with pagination support @@ -1706,6 +1710,12 @@ async def ui_view_spend_logs( # noqa: PLR0915 if team_id is not None: where_conditions["team_id"] = team_id + if status_filter is not None: + if status_filter == "success": + where_conditions["status"] = {"in": ["success", None]} # Assuming None means empty status + else: + where_conditions["status"] = status_filter # Filtering for other status values + if api_key is not None: where_conditions["api_key"] = api_key diff --git a/ui/litellm-dashboard/src/components/common_components/filter.tsx b/ui/litellm-dashboard/src/components/common_components/filter.tsx index 1155a04192..067fadfc6b 100644 --- a/ui/litellm-dashboard/src/components/common_components/filter.tsx +++ b/ui/litellm-dashboard/src/components/common_components/filter.tsx @@ -1,13 +1,16 @@ -import React, { useState, useCallback } from 'react'; -import { Button, Input, Select } from 'antd'; -import { FilterIcon } from '@heroicons/react/outline'; -import debounce from 'lodash/debounce'; +import React, { useState, useCallback } from "react"; +import { Button, Input, Select } from "antd"; +import { FilterIcon } from "@heroicons/react/outline"; +import debounce from "lodash/debounce"; export interface FilterOption { name: string; label?: string; isSearchable?: boolean; - searchFn?: (searchText: string) => Promise>; + searchFn?: ( + searchText: string + ) => Promise>; + options?: Array<{ label: string; value: string }>; } interface FilterValues { @@ -31,23 +34,29 @@ const FilterComponent: React.FC = ({ }) => { const [showFilters, setShowFilters] = useState(false); const [tempValues, setTempValues] = useState(initialValues); - const [searchOptionsMap, setSearchOptionsMap] = useState<{ [key: string]: Array<{ label: string; value: string }> }>({}); - const [searchLoadingMap, setSearchLoadingMap] = useState<{ [key: string]: boolean }>({}); - const [searchInputValueMap, setSearchInputValueMap] = useState<{ [key: string]: string }>({}); + const [searchOptionsMap, setSearchOptionsMap] = useState<{ + [key: string]: Array<{ label: string; value: string }>; + }>({}); + const [searchLoadingMap, setSearchLoadingMap] = useState<{ + [key: string]: boolean; + }>({}); + const [searchInputValueMap, setSearchInputValueMap] = useState<{ + [key: string]: string; + }>({}); const debouncedSearch = useCallback( debounce(async (value: string, option: FilterOption) => { if (!option.isSearchable || !option.searchFn) return; - - setSearchLoadingMap(prev => ({ ...prev, [option.name]: true })); + + setSearchLoadingMap((prev) => ({ ...prev, [option.name]: true })); try { const results = await option.searchFn(value); - setSearchOptionsMap(prev => ({ ...prev, [option.name]: results })); + setSearchOptionsMap((prev) => ({ ...prev, [option.name]: results })); } catch (error) { - console.error('Error searching:', error); - setSearchOptionsMap(prev => ({ ...prev, [option.name]: [] })); + console.error("Error searching:", error); + setSearchOptionsMap((prev) => ({ ...prev, [option.name]: [] })); } finally { - setSearchLoadingMap(prev => ({ ...prev, [option.name]: false })); + setSearchLoadingMap((prev) => ({ ...prev, [option.name]: false })); } }, 300), [] @@ -56,7 +65,7 @@ const FilterComponent: React.FC = ({ const handleFilterChange = (name: string, value: string) => { const newValues = { ...tempValues, - [name]: value + [name]: value, }; setTempValues(newValues); onApplyFilters(newValues); @@ -64,8 +73,8 @@ const FilterComponent: React.FC = ({ const resetFilters = () => { const emptyValues: FilterValues = {}; - options.forEach(option => { - emptyValues[option.name] = ''; + options.forEach((option) => { + emptyValues[option.name] = ""; }); setTempValues(emptyValues); onResetFilters(); @@ -73,17 +82,18 @@ const FilterComponent: React.FC = ({ // Define the order of filters const orderedFilters = [ - 'Team ID', - 'Organization ID', - 'Key Alias', - 'User ID', - 'Key Hash' + "Team ID", + "Status", + "Organization ID", + "Key Alias", + "User ID", + "Key Hash", ]; return (
- - - {showFilters && ( -
-
-
- Where -
- - {showColumnDropdown && ( -
- {["Team ID", "Key Hash"].map((option) => ( - - ))} -
- )} -
- { - if (selectedFilter === "Team ID") { - setTempTeamId(e.target.value); - } else { - setTempKeyHash(e.target.value); - } - }} - /> - -
- -
- - -
-
-
- )} -
@@ -630,20 +559,20 @@ export default function SpendLogsTable({ Showing{" "} {logs.isLoading ? "..." - : logs.data + : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0}{" "} -{" "} {logs.isLoading ? "..." - : logs.data - ? Math.min(currentPage * pageSize, logs.data.total) + : filteredLogs + ? Math.min(currentPage * pageSize, filteredLogs.total) : 0}{" "} of{" "} {logs.isLoading ? "..." - : logs.data - ? logs.data.total + : filteredLogs + ? filteredLogs.total : 0}{" "} results @@ -652,8 +581,8 @@ export default function SpendLogsTable({ Page {logs.isLoading ? "..." : currentPage} of{" "} {logs.isLoading ? "..." - : logs.data - ? logs.data.total_pages + : filteredLogs + ? filteredLogs.total_pages : 1}