mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
(beta ui - spend logs view fixes & Improvements 1) (#8062)
* ui 1 - show correct msg on no logs * fix dup country col * backend - allow filtering by team_id and api_key * fix ui_view_spend_logs * ui update query params * working team id and key hash filters * fix filter ref - don't hold on them as they are * fix _model_custom_llm_provider_matches_wildcard_pattern * fix test test_stream_chunk_builder_openai_audio_output_usage - use direct dict comparison
This commit is contained in:
parent
311997ee40
commit
ae7b042bc2
6 changed files with 246 additions and 181 deletions
|
@ -1627,7 +1627,19 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
),
|
),
|
||||||
request_id: Optional[str] = fastapi.Query(
|
request_id: Optional[str] = fastapi.Query(
|
||||||
default=None,
|
default=None,
|
||||||
description="request_id to get spend logs for specific request_id. If none passed then pass spend logs for all requests",
|
description="request_id to get spend logs for specific request_id",
|
||||||
|
),
|
||||||
|
team_id: Optional[str] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Filter spend logs by team_id",
|
||||||
|
),
|
||||||
|
min_spend: Optional[float] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Filter logs with spend greater than or equal to this value",
|
||||||
|
),
|
||||||
|
max_spend: Optional[float] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Filter logs with spend less than or equal to this value",
|
||||||
),
|
),
|
||||||
start_date: Optional[str] = fastapi.Query(
|
start_date: Optional[str] = fastapi.Query(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -1674,46 +1686,70 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
param="None",
|
param="None",
|
||||||
code=status.HTTP_400_BAD_REQUEST,
|
code=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# Convert the date strings to datetime objects
|
|
||||||
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
|
|
||||||
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
# Convert to ISO format strings for Prisma
|
try:
|
||||||
start_date_iso = start_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
# Convert the date strings to datetime objects
|
||||||
end_date_iso = end_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
|
||||||
|
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
# Calculate skip value for pagination
|
# Convert to ISO format strings for Prisma
|
||||||
skip = (page - 1) * page_size
|
start_date_iso = start_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
||||||
|
end_date_iso = end_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
||||||
|
|
||||||
# Get total count of records
|
# Build where conditions
|
||||||
total_records = await prisma_client.db.litellm_spendlogs.count(
|
where_conditions: dict[str, Any] = {
|
||||||
where={
|
|
||||||
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
# Get paginated data
|
if team_id is not None:
|
||||||
data = await prisma_client.db.litellm_spendlogs.find_many(
|
where_conditions["team_id"] = team_id
|
||||||
where={
|
|
||||||
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
|
||||||
},
|
|
||||||
order={
|
|
||||||
"startTime": "desc",
|
|
||||||
},
|
|
||||||
skip=skip,
|
|
||||||
take=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate total pages
|
if api_key is not None:
|
||||||
total_pages = (total_records + page_size - 1) // page_size
|
where_conditions["api_key"] = api_key
|
||||||
|
|
||||||
return {
|
if user_id is not None:
|
||||||
"data": data,
|
where_conditions["user"] = user_id
|
||||||
"total": total_records,
|
|
||||||
"page": page,
|
if request_id is not None:
|
||||||
"page_size": page_size,
|
where_conditions["request_id"] = request_id
|
||||||
"total_pages": total_pages,
|
|
||||||
}
|
if min_spend is not None or max_spend is not None:
|
||||||
|
where_conditions["spend"] = {}
|
||||||
|
if min_spend is not None:
|
||||||
|
where_conditions["spend"]["gte"] = min_spend
|
||||||
|
if max_spend is not None:
|
||||||
|
where_conditions["spend"]["lte"] = max_spend
|
||||||
|
# Calculate skip value for pagination
|
||||||
|
skip = (page - 1) * page_size
|
||||||
|
|
||||||
|
# Get total count of records
|
||||||
|
total_records = await prisma_client.db.litellm_spendlogs.count(
|
||||||
|
where=where_conditions,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get paginated data
|
||||||
|
data = await prisma_client.db.litellm_spendlogs.find_many(
|
||||||
|
where=where_conditions,
|
||||||
|
order={
|
||||||
|
"startTime": "desc",
|
||||||
|
},
|
||||||
|
skip=skip,
|
||||||
|
take=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate total pages
|
||||||
|
total_pages = (total_records + page_size - 1) // page_size
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": data,
|
||||||
|
"total": total_records,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
verbose_proxy_logger.exception(f"Error in ui_view_spend_logs: {e}")
|
||||||
|
raise handle_exception_on_proxy(e)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|
|
@ -721,22 +721,14 @@ def test_stream_chunk_builder_openai_audio_output_usage():
|
||||||
print(f"response usage: {response.usage}")
|
print(f"response usage: {response.usage}")
|
||||||
check_non_streaming_response(response)
|
check_non_streaming_response(response)
|
||||||
print(f"response: {response}")
|
print(f"response: {response}")
|
||||||
for k, v in usage_obj.model_dump(exclude_none=True).items():
|
# Convert both usage objects to dictionaries for easier comparison
|
||||||
print(k, v)
|
usage_dict = usage_obj.model_dump(exclude_none=True)
|
||||||
response_usage_value = getattr(response.usage, k) # type: ignore
|
response_usage_dict = response.usage.model_dump(exclude_none=True)
|
||||||
print(f"response_usage_value: {response_usage_value}")
|
|
||||||
print(f"type: {type(response_usage_value)}")
|
# Simple dictionary comparison
|
||||||
if isinstance(response_usage_value, BaseModel):
|
assert (
|
||||||
response_usage_value_dict = response_usage_value.model_dump(
|
usage_dict == response_usage_dict
|
||||||
exclude_none=True
|
), f"\nExpected: {usage_dict}\nGot: {response_usage_dict}"
|
||||||
)
|
|
||||||
if isinstance(v, dict):
|
|
||||||
for key, value in v.items():
|
|
||||||
assert response_usage_value_dict[key] == value
|
|
||||||
else:
|
|
||||||
assert response_usage_value_dict == v
|
|
||||||
else:
|
|
||||||
assert response_usage_value == v
|
|
||||||
|
|
||||||
|
|
||||||
def test_stream_chunk_builder_empty_initial_chunk():
|
def test_stream_chunk_builder_empty_initial_chunk():
|
||||||
|
|
|
@ -1553,12 +1553,14 @@ export const userSpendLogsCall = async (
|
||||||
export const uiSpendLogsCall = async (
|
export const uiSpendLogsCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
api_key?: string,
|
api_key?: string,
|
||||||
user_id?: string,
|
team_id?: string,
|
||||||
request_id?: string,
|
request_id?: string,
|
||||||
start_date?: string,
|
start_date?: string,
|
||||||
end_date?: string,
|
end_date?: string,
|
||||||
page?: number,
|
page?: number,
|
||||||
page_size?: number,
|
page_size?: number,
|
||||||
|
min_spend?: number,
|
||||||
|
max_spend?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Construct base URL
|
// Construct base URL
|
||||||
|
@ -1567,7 +1569,9 @@ export const uiSpendLogsCall = async (
|
||||||
// Add query parameters if they exist
|
// Add query parameters if they exist
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (api_key) queryParams.append('api_key', api_key);
|
if (api_key) queryParams.append('api_key', api_key);
|
||||||
if (user_id) queryParams.append('user_id', user_id);
|
if (team_id) queryParams.append('team_id', team_id);
|
||||||
|
if (min_spend) queryParams.append('min_spend', min_spend.toString());
|
||||||
|
if (max_spend) queryParams.append('max_spend', max_spend.toString());
|
||||||
if (request_id) queryParams.append('request_id', request_id);
|
if (request_id) queryParams.append('request_id', request_id);
|
||||||
if (start_date) queryParams.append('start_date', start_date);
|
if (start_date) queryParams.append('start_date', start_date);
|
||||||
if (end_date) queryParams.append('end_date', end_date);
|
if (end_date) queryParams.append('end_date', end_date);
|
||||||
|
@ -1595,7 +1599,7 @@ export const uiSpendLogsCall = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Spend Logs UI Response:", data);
|
console.log("Spend Logs Response:", data);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch spend logs:", error);
|
console.error("Failed to fetch spend logs:", error);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { CountryCell } from "./country_cell";
|
||||||
export type LogEntry = {
|
export type LogEntry = {
|
||||||
request_id: string;
|
request_id: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
|
team_id: string;
|
||||||
model: string;
|
model: string;
|
||||||
api_base?: string;
|
api_base?: string;
|
||||||
call_type: string;
|
call_type: string;
|
||||||
|
@ -155,11 +156,6 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: "Country",
|
|
||||||
accessorKey: "requester_ip_address",
|
|
||||||
cell: (info: any) => <CountryCell ipAddress={info.getValue()} />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatMessage = (message: any): string => {
|
const formatMessage = (message: any): string => {
|
||||||
|
|
|
@ -29,14 +29,13 @@ export default function SpendLogsTable({
|
||||||
userID,
|
userID,
|
||||||
}: SpendLogsTableProps) {
|
}: SpendLogsTableProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [keyNameFilter, setKeyNameFilter] = useState("");
|
|
||||||
const [teamNameFilter, setTeamNameFilter] = useState("");
|
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
|
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
|
||||||
const [selectedColumn, setSelectedColumn] = useState("Key Name");
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize] = useState(50);
|
const [pageSize] = useState(50);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const filtersRef = useRef<HTMLDivElement>(null);
|
||||||
|
const quickSelectRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// New state variables for Start and End Time
|
// New state variables for Start and End Time
|
||||||
const [startTime, setStartTime] = useState<string>(
|
const [startTime, setStartTime] = useState<string>(
|
||||||
|
@ -49,6 +48,11 @@ export default function SpendLogsTable({
|
||||||
// Add these new state variables at the top with other useState declarations
|
// Add these new state variables at the top with other useState declarations
|
||||||
const [isCustomDate, setIsCustomDate] = useState(false);
|
const [isCustomDate, setIsCustomDate] = useState(false);
|
||||||
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
|
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
|
||||||
|
const [tempTeamId, setTempTeamId] = useState("");
|
||||||
|
const [tempKeyHash, setTempKeyHash] = useState("");
|
||||||
|
const [selectedTeamId, setSelectedTeamId] = useState("");
|
||||||
|
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -59,6 +63,18 @@ export default function SpendLogsTable({
|
||||||
) {
|
) {
|
||||||
setShowColumnDropdown(false);
|
setShowColumnDropdown(false);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
filtersRef.current &&
|
||||||
|
!filtersRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowFilters(false);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
quickSelectRef.current &&
|
||||||
|
!quickSelectRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setQuickSelectOpen(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
@ -67,12 +83,19 @@ export default function SpendLogsTable({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logs = useQuery<PaginatedResponse>({
|
const logs = useQuery<PaginatedResponse>({
|
||||||
queryKey: ["logs", "table", currentPage, pageSize, startTime, endTime],
|
queryKey: [
|
||||||
|
"logs",
|
||||||
|
"table",
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
selectedTeamId,
|
||||||
|
selectedKeyHash,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
console.log(
|
console.log("Missing required auth parameters");
|
||||||
"got None values for one of accessToken, token, userRole, userID",
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
data: [],
|
data: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -85,20 +108,17 @@ export default function SpendLogsTable({
|
||||||
const formattedStartTime = moment(startTime).format("YYYY-MM-DD HH:mm:ss");
|
const formattedStartTime = moment(startTime).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const formattedEndTime = moment(endTime).format("YYYY-MM-DD HH:mm:ss");
|
const formattedEndTime = moment(endTime).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
|
||||||
const data = await uiSpendLogsCall(
|
return await uiSpendLogsCall(
|
||||||
accessToken,
|
accessToken,
|
||||||
token,
|
selectedKeyHash || undefined,
|
||||||
userRole,
|
selectedTeamId || undefined,
|
||||||
userID,
|
undefined, // This parameter might be setting a default min_spend
|
||||||
formattedStartTime,
|
formattedStartTime,
|
||||||
formattedEndTime,
|
formattedEndTime,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
},
|
||||||
// Refetch when startTime or endTime changes
|
|
||||||
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,21 +136,12 @@ export default function SpendLogsTable({
|
||||||
log.request_id.includes(searchTerm) ||
|
log.request_id.includes(searchTerm) ||
|
||||||
log.model.includes(searchTerm) ||
|
log.model.includes(searchTerm) ||
|
||||||
(log.user && log.user.includes(searchTerm));
|
(log.user && log.user.includes(searchTerm));
|
||||||
const matchesKeyName =
|
|
||||||
!keyNameFilter ||
|
// No need for additional filtering since we're now handling this in the API call
|
||||||
(log.metadata?.user_api_key_alias &&
|
return matchesSearch;
|
||||||
log.metadata.user_api_key_alias
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(keyNameFilter.toLowerCase()));
|
|
||||||
const matchesTeamName =
|
|
||||||
!teamNameFilter ||
|
|
||||||
(log.metadata?.user_api_key_team_alias &&
|
|
||||||
log.metadata.user_api_key_team_alias
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(teamNameFilter.toLowerCase()));
|
|
||||||
return matchesSearch && matchesKeyName && matchesTeamName;
|
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h1 className="text-xl font-semibold mb-4">Traces</h1>
|
<h1 className="text-xl font-semibold mb-4">Traces</h1>
|
||||||
|
@ -160,7 +171,7 @@ export default function SpendLogsTable({
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={filtersRef}>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
|
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
@ -183,100 +194,123 @@ export default function SpendLogsTable({
|
||||||
|
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<div className="absolute left-0 mt-2 w-[500px] bg-white rounded-lg shadow-lg border p-4 z-50">
|
<div className="absolute left-0 mt-2 w-[500px] bg-white rounded-lg shadow-lg border p-4 z-50">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<span className="text-sm font-medium">Where</span>
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<span className="text-sm font-medium">Where</span>
|
||||||
<button
|
<div className="relative">
|
||||||
onClick={() =>
|
<button
|
||||||
setShowColumnDropdown(!showColumnDropdown)
|
onClick={() => setShowColumnDropdown(!showColumnDropdown)}
|
||||||
}
|
className="px-3 py-1.5 border rounded-md bg-white text-sm min-w-[160px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-left flex justify-between items-center"
|
||||||
className="px-3 py-1.5 border rounded-md bg-white text-sm min-w-[160px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-left flex justify-between items-center"
|
|
||||||
>
|
|
||||||
{selectedColumn}
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-gray-500"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<path
|
{selectedFilter}
|
||||||
strokeLinecap="round"
|
<svg
|
||||||
strokeLinejoin="round"
|
className="h-4 w-4 text-gray-500"
|
||||||
strokeWidth={2}
|
fill="none"
|
||||||
d="M19 9l-7 7-7-7"
|
stroke="currentColor"
|
||||||
/>
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{showColumnDropdown && (
|
||||||
|
<div className="absolute left-0 mt-1 w-[160px] bg-white border rounded-md shadow-lg z-50">
|
||||||
|
{["Team ID", "Key Hash"].map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2 ${
|
||||||
|
selectedFilter === option
|
||||||
|
? "bg-blue-50 text-blue-600"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedFilter(option);
|
||||||
|
setShowColumnDropdown(false);
|
||||||
|
if (option === "Team ID") {
|
||||||
|
setTempKeyHash("");
|
||||||
|
} else {
|
||||||
|
setTempTeamId("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedFilter === option && (
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 text-blue-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter value..."
|
||||||
|
className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
value={selectedFilter === "Team ID" ? tempTeamId : tempKeyHash}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (selectedFilter === "Team ID") {
|
||||||
|
setTempTeamId(e.target.value);
|
||||||
|
} else {
|
||||||
|
setTempKeyHash(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-100 rounded-md"
|
||||||
|
onClick={() => {
|
||||||
|
setTempTeamId("");
|
||||||
|
setTempKeyHash("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-gray-500">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
className="px-3 py-1.5 text-sm border rounded-md hover:bg-gray-50"
|
||||||
|
onClick={() => {
|
||||||
|
setTempTeamId("");
|
||||||
|
setTempKeyHash("");
|
||||||
|
setShowFilters(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTeamId(tempTeamId);
|
||||||
|
setSelectedKeyHash(tempKeyHash);
|
||||||
|
setCurrentPage(1); // Reset to first page when applying new filters
|
||||||
|
setShowFilters(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply Filters
|
||||||
</button>
|
</button>
|
||||||
{showColumnDropdown && (
|
|
||||||
<div className="absolute left-0 mt-1 w-[160px] bg-white border rounded-md shadow-lg z-50">
|
|
||||||
{["Key Name", "Team Name"].map((option) => (
|
|
||||||
<button
|
|
||||||
key={option}
|
|
||||||
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2 ${
|
|
||||||
selectedColumn === option
|
|
||||||
? "bg-blue-50 text-blue-600"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedColumn(option);
|
|
||||||
setShowColumnDropdown(false);
|
|
||||||
if (option === "Key Name") {
|
|
||||||
setTeamNameFilter("");
|
|
||||||
} else {
|
|
||||||
setKeyNameFilter("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedColumn === option && (
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-blue-600"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{option}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter value..."
|
|
||||||
className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
value={keyNameFilter || teamNameFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (selectedColumn === "Key Name") {
|
|
||||||
setKeyNameFilter(e.target.value);
|
|
||||||
} else {
|
|
||||||
setTeamNameFilter(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="p-1 hover:bg-gray-100 rounded-md"
|
|
||||||
onClick={() => {
|
|
||||||
setKeyNameFilter("");
|
|
||||||
setTeamNameFilter("");
|
|
||||||
setShowFilters(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-gray-500">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative" ref={quickSelectRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setQuickSelectOpen(!quickSelectOpen)}
|
onClick={() => setQuickSelectOpen(!quickSelectOpen)}
|
||||||
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
|
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
|
||||||
|
|
|
@ -22,6 +22,7 @@ interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
renderSubComponent: (props: { row: Row<TData> }) => React.ReactElement;
|
renderSubComponent: (props: { row: Row<TData> }) => React.ReactElement;
|
||||||
getRowCanExpand: (row: Row<TData>) => boolean;
|
getRowCanExpand: (row: Row<TData>) => boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
|
@ -29,6 +30,7 @@ export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
getRowCanExpand,
|
getRowCanExpand,
|
||||||
renderSubComponent,
|
renderSubComponent,
|
||||||
|
isLoading = false,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
@ -60,7 +62,15 @@ export function DataTable<TData, TValue>({
|
||||||
))}
|
))}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows.length > 0 ? (
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
<div className="p-8 text-center text-gray-500">
|
||||||
|
<p>🚅 Loading logs...</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : table.getRowModel().rows.length > 0 ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<Fragment key={row.id}>
|
<Fragment key={row.id}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
@ -87,14 +97,7 @@ export function DataTable<TData, TValue>({
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
<p>No SpendLogs messages available.</p>
|
<p>No logs found</p>
|
||||||
<p className="text-sm mt-2">
|
|
||||||
To enable this, set{" "}
|
|
||||||
<code>
|
|
||||||
`general_settings.store_prompts_in_spend_logs: true`
|
|
||||||
</code>{" "}
|
|
||||||
in your config.yaml
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue