diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index e49ad6bf2d..88a66c9c58 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -22,6 +22,7 @@ "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "lucide-react": "^0.513.0", "moment": "^2.30.1", "next": "^14.2.26", "openai": "^4.93.0", @@ -557,7 +558,6 @@ "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -4352,6 +4352,14 @@ "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.513.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", + "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", @@ -7993,21 +8001,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.26", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz", - "integrity": "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 356f3bd0e0..0406d55fda 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -23,6 +23,7 @@ "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "lucide-react": "^0.513.0", "moment": "^2.30.1", "next": "^14.2.26", "openai": "^4.93.0", diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 66f36b0348..987504538c 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -405,7 +405,8 @@ export default function CreateKeyPage() { userRole={userRole} token={token} accessToken={accessToken} - allTeams={(teams as Team[]) ?? []} + allTeams={teams as Team[] ?? []} + premiumUser={premiumUser} /> ) : page == "mcp-servers" ? ( { + try { + // Construct base URL + let url = proxyBaseUrl ? `${proxyBaseUrl}/audit` : `/audit`; + + // Add query parameters if they exist + const queryParams = new URLSearchParams(); + // if (start_date) queryParams.append('start_date', start_date); + // if (end_date) queryParams.append('end_date', end_date); + if (page) queryParams.append('page', page.toString()); + if (page_size) queryParams.append('page_size', page_size.toString()); + + // Append query parameters to URL if any exist + const queryString = queryParams.toString(); + if (queryString) { + url += `?${queryString}`; + } + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to fetch spend logs:", error); + throw error; + } +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx new file mode 100644 index 0000000000..d98640375d --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx @@ -0,0 +1,568 @@ +import { DataTable } from "./table"; +import moment from "moment"; +import { useRef, useState, useEffect, useCallback, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { uiAuditLogsCall, keyListCall } from "../networking"; +import { AuditLogEntry, auditLogColumns } from "./columns"; +import { Text } from "@tremor/react"; +import { Team } from "../key_team_helpers/key_list"; + +interface AuditLogsProps { + accessToken: string | null; + token: string | null; + userRole: string | null; + userID: string | null; + isActive: boolean; + premiumUser: boolean; + allTeams: Team[]; +} + +export default function AuditLogs({ + userID, + userRole, + token, + accessToken, + isActive, + premiumUser, + allTeams, +}: AuditLogsProps) { + const [startTime, setStartTime] = useState( + moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") + ); + + const actionFilterRef = useRef(null); + const tableFilterRef = useRef(null); + const [clientCurrentPage, setClientCurrentPage] = useState(1); + const [pageSize] = useState(50); + const [filters, setFilters] = useState>({}); + const [selectedTeamId, setSelectedTeamId] = useState(""); + const [selectedKeyHash, setSelectedKeyHash] = useState(""); + const [objectIdSearch, setObjectIdSearch] = useState(""); + const [selectedActionFilter, setSelectedActionFilter] = useState("all"); + const [selectedTableFilter, setSelectedTableFilter] = useState("all"); + const [actionFilterOpen, setActionFilterOpen] = useState(false); + const [tableFilterOpen, setTableFilterOpen] = useState(false); + + const allLogsQuery = useQuery({ + queryKey: [ + "all_audit_logs", + accessToken, + token, + userRole, + userID, + startTime, + ], + queryFn: async () => { + if (!accessToken || !token || !userRole || !userID) { + return []; + } + + const formattedStartTimeStr = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss"); + const formattedEndTimeStr = moment().utc().format("YYYY-MM-DD HH:mm:ss"); + + let accumulatedLogs: AuditLogEntry[] = []; + let currentPageToFetch = 1; + let totalPagesFromBackend = 1; + const backendPageSize = 50; + + do { + const response = await uiAuditLogsCall( + accessToken, + formattedStartTimeStr, + formattedEndTimeStr, + currentPageToFetch, + backendPageSize + ); + accumulatedLogs = accumulatedLogs.concat(response.audit_logs); + totalPagesFromBackend = response.total_pages; + currentPageToFetch++; + } while (currentPageToFetch <= totalPagesFromBackend); + + return accumulatedLogs; + }, + enabled: !!accessToken && !!token && !!userRole && !!userID && isActive, + refetchInterval: 5000, + refetchIntervalInBackground: true, + }); + + const handleRefresh = () => { + allLogsQuery.refetch(); + }; + + const handleFilterChange = (newFilters: Record) => { + setFilters(newFilters); + }; + + const handleFilterReset = () => { + setFilters({}); + setSelectedTeamId(""); + setSelectedKeyHash(""); + setObjectIdSearch(""); + setSelectedActionFilter("all"); + setSelectedTableFilter("all"); + setClientCurrentPage(1); + }; + + const fetchKeyHashForAlias = useCallback(async (keyAlias: string) => { + if (!accessToken) return; + + try { + const response = await keyListCall( + accessToken, + null, + null, + keyAlias, + null, + null, + 1, + 10 + ); + + const selectedKey = response.keys.find( + (key: any) => key.key_alias === keyAlias + ); + + if (selectedKey) { + setSelectedKeyHash(selectedKey.token); + } else { + setSelectedKeyHash(""); + } + } catch (error) { + console.error("Error fetching key hash for alias:", error); + setSelectedKeyHash(""); + } + }, [accessToken]); + + useEffect(() => { + if(!accessToken) return; + + let teamIdChanged = false; + let keyHashChanged = false; + + if (filters['Team ID']) { + if (selectedTeamId !== filters['Team ID']) { + setSelectedTeamId(filters['Team ID']); + teamIdChanged = true; + } + } else { + if (selectedTeamId !== "") { + setSelectedTeamId(""); + teamIdChanged = true; + } + } + + if (filters['Key Hash']) { + if (selectedKeyHash !== filters['Key Hash']) { + setSelectedKeyHash(filters['Key Hash']); + keyHashChanged = true; + } + } else if (filters['Key Alias']) { + fetchKeyHashForAlias(filters['Key Alias']); + } else { + if (selectedKeyHash !== "") { + setSelectedKeyHash(""); + keyHashChanged = true; + } + } + + if (teamIdChanged || keyHashChanged) { + setClientCurrentPage(1); + } + }, [filters, accessToken, fetchKeyHashForAlias, selectedTeamId, selectedKeyHash]); + + useEffect(() => { + setClientCurrentPage(1); + }, [selectedTeamId, selectedKeyHash, startTime, objectIdSearch, selectedActionFilter, selectedTableFilter]); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + actionFilterRef.current && + !actionFilterRef.current.contains(event.target as Node) + ) { + setActionFilterOpen(false); + } + if ( + tableFilterRef.current && + !tableFilterRef.current.contains(event.target as Node) + ) { + setTableFilterOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const completeFilteredLogs = useMemo(() => { + if (!allLogsQuery.data) return []; + return allLogsQuery.data.filter(log => { + let matchesTeam = true; + let matchesKey = true; + let matchesObjectId = true; + let matchesAction = true; + let matchesTable = true; + + if (selectedTeamId) { + const beforeTeamId = typeof log.before_value === 'string' ? JSON.parse(log.before_value)?.team_id : log.before_value?.team_id; + const updatedTeamId = typeof log.updated_values === 'string' ? JSON.parse(log.updated_values)?.team_id : log.updated_values?.team_id; + matchesTeam = beforeTeamId === selectedTeamId || updatedTeamId === selectedTeamId; + } + + if (selectedKeyHash) { + try { + const beforeBody = typeof log.before_value === 'string' ? JSON.parse(log.before_value) : log.before_value; + const updatedBody = typeof log.updated_values === 'string' ? JSON.parse(log.updated_values) : log.updated_values; + + const beforeKey = beforeBody?.token; + const updatedKey = updatedBody?.token; + + matchesKey = (typeof beforeKey === 'string' && beforeKey.includes(selectedKeyHash)) || + (typeof updatedKey === 'string' && updatedKey.includes(selectedKeyHash)); + } catch (e) { + matchesKey = false; + } + } + + if (objectIdSearch) { + matchesObjectId = log.object_id?.toLowerCase().includes(objectIdSearch.toLowerCase()); + } + + if (selectedActionFilter !== "all") { + matchesAction = log.action?.toLowerCase() === selectedActionFilter.toLowerCase(); + } + + if (selectedTableFilter !== "all") { + let tableMatchName = ""; + switch(selectedTableFilter) { + case "keys": tableMatchName = "litellm_verificationtoken"; break; + case "teams": tableMatchName = "litellm_teamtable"; break; + case "users": tableMatchName = "litellm_usertable"; break; + // Add other direct table names if needed, or rely on a more generic match + default: tableMatchName = selectedTableFilter; // Should not happen with current UI options + } + matchesTable = log.table_name?.toLowerCase() === tableMatchName; + } + + return matchesTeam && matchesKey && matchesObjectId && matchesAction && matchesTable; + }); + }, [allLogsQuery.data, selectedTeamId, selectedKeyHash, objectIdSearch, selectedActionFilter, selectedTableFilter]); + + const totalFilteredItems = completeFilteredLogs.length; + const totalFilteredPages = Math.ceil(totalFilteredItems / pageSize) || 1; + + const paginatedViewOfFilteredLogs = useMemo(() => { + const start = (clientCurrentPage - 1) * pageSize; + const end = start + pageSize; + return completeFilteredLogs.slice(start, end); + }, [completeFilteredLogs, clientCurrentPage, pageSize]); + + const renderSubComponent = useCallback(({ row }: { row: any }) => { + const AuditLogRowExpansionPanel = ({ rowData }: { rowData: AuditLogEntry }) => { + const { before_value, updated_values, table_name, action } = rowData; + + const renderValue = (value: Record, isKeyTable: boolean) => { + if (!value || Object.keys(value).length === 0) return N/A; + + if (isKeyTable) { + const changedKeys = Object.keys(value); + const knownKeyFields = ['token', 'spend', 'max_budget']; + + const onlyKnownFieldsChanged = changedKeys.every(key => knownKeyFields.includes(key)); + + if (onlyKnownFieldsChanged && changedKeys.length > 0) { + return ( +
+ {changedKeys.includes('token') &&

Token: {value.token || 'N/A'}

} + {changedKeys.includes('spend') &&

Spend: {value.spend !== undefined ? `$${Number(value.spend).toFixed(6)}` : 'N/A'}

} + {changedKeys.includes('max_budget') &&

Max Budget: {value.max_budget !== undefined ? `$${Number(value.max_budget).toFixed(6)}` : 'N/A'}

} +
+ ); + } else { + if (value["No differing fields detected in 'before' state"] || value["No differing fields detected in 'updated' state"] || value["No fields changed"]) { + return {value[Object.keys(value)[0]]} // Display the N/A message string + } + return
{JSON.stringify(value, null, 2)}
; + } + } + + return
{JSON.stringify(value, null, 2)}
; + }; + + let displayBeforeValue = before_value; + let displayUpdatedValue = updated_values; + + if ((action === "updated" || action === "rotated") && before_value && updated_values) { + if (table_name === "LiteLLM_TeamTable" || table_name === "LiteLLM_UserTable" || table_name === "LiteLLM_VerificationToken") { + + const changedBefore: Record = {}; + const changedUpdated: Record = {}; + const allKeys = new Set([...Object.keys(before_value), ...Object.keys(updated_values)]); + + allKeys.forEach(key => { + const beforeValStr = JSON.stringify(before_value[key]); + const updatedValStr = JSON.stringify(updated_values[key]); + if (beforeValStr !== updatedValStr) { + if (before_value.hasOwnProperty(key)) { + changedBefore[key] = before_value[key]; + } + if (updated_values.hasOwnProperty(key)) { + changedUpdated[key] = updated_values[key]; + } + } + }); + + Object.keys(before_value).forEach(key => { + if (!updated_values.hasOwnProperty(key) && !changedBefore.hasOwnProperty(key)) { + changedBefore[key] = before_value[key]; + changedUpdated[key] = undefined; + } + }); + + Object.keys(updated_values).forEach(key => { + if (!before_value.hasOwnProperty(key) && !changedUpdated.hasOwnProperty(key)) { + changedUpdated[key] = updated_values[key]; + changedBefore[key] = undefined; + } + }); + + displayBeforeValue = Object.keys(changedBefore).length > 0 ? changedBefore : { "No differing fields detected in 'before' state": "N/A" }; + displayUpdatedValue = Object.keys(changedUpdated).length > 0 ? changedUpdated : { "No differing fields detected in 'updated' state": "N/A" }; + + if (Object.keys(changedBefore).length === 0 && Object.keys(changedUpdated).length === 0) { + displayBeforeValue = {"No fields changed": "N/A"}; + displayUpdatedValue = {"No fields changed": "N/A"}; + } + } + } + + return ( +
+
+

Before Value:

+ {renderValue(displayBeforeValue, table_name === "LiteLLM_VerificationToken")} +
+
+

Updated Value:

+ {renderValue(displayUpdatedValue, table_name === "LiteLLM_VerificationToken")} +
+
+ ); + }; + + return ; + }, []); + + if (!premiumUser) { + return ( +
+ This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key here. +
+ ); + } + + const currentDisplayItemsStart = totalFilteredItems > 0 ? (clientCurrentPage - 1) * pageSize + 1 : 0; + const currentDisplayItemsEnd = Math.min(clientCurrentPage * pageSize, totalFilteredItems); + + return ( + <> +
+ +
+ {/* */} +
+
+

+ Audit Logs +

+
+ +
+ +
+
+ setObjectIdSearch(e.target.value)} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + +
+ +
+ +
+ {/* Custom Action Filter Dropdown */} +
+ + + {actionFilterOpen && ( +
+
+ {[ + { label: "All Actions", value: "all" }, + { label: "Created", value: "created" }, + { label: "Updated", value: "updated" }, + { label: "Deleted", value: "deleted" }, + { label: "Rotated", value: "rotated" }, + ].map((option) => ( + + ))} +
+
+ )} +
+ + {/* Custom Table Filter Dropdown */} +
+ + + {tableFilterOpen && ( +
+
+ {[ + { label: "All Tables", value: "all" }, + { label: "Keys", value: "keys" }, + { label: "Teams", value: "teams" }, + { label: "Users", value: "users" }, + ].map((option) => ( + + ))} +
+
+ )} +
+ + + Showing{" "} + {allLogsQuery.isLoading + ? "..." + : currentDisplayItemsStart}{" "} + -{" "} + {allLogsQuery.isLoading + ? "..." + : currentDisplayItemsEnd}{" "} + of{" "} + {allLogsQuery.isLoading + ? "..." + : totalFilteredItems}{" "} + results + +
+ + Page {allLogsQuery.isLoading ? "..." : clientCurrentPage} of{" "} + {allLogsQuery.isLoading + ? "..." + : totalFilteredPages} + + + +
+
+
+
+ true} + /> +
+ + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 54670bbab5..95a38853fa 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -1,12 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table"; -import { getCountryFromIP } from "./ip_lookup"; -import moment from "moment"; -import React from "react"; -import { CountryCell } from "./country_cell"; +import React, { useState } from "react"; import { getProviderLogoAndName } from "../provider_info_helpers"; import { Tooltip } from "antd"; import { TimeCell } from "./time_cell"; -import { Button } from "@tremor/react"; +import { Button, Badge } from "@tremor/react"; +import { Eye, EyeOff} from "lucide-react" // Helper to get the appropriate logo URL const getLogoUrl = ( @@ -361,3 +359,189 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp ); }; + +// New component for collapsible JSON display +const CollapsibleJsonCell = ({ jsonData }: { jsonData: any }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + const jsonString = JSON.stringify(jsonData, null, 2); + + if (!jsonData || Object.keys(jsonData).length === 0) { + return -; + } + + return ( +
+ + {isExpanded && ( +
+          {jsonString}
+        
+ )} +
+ ); +}; + +export type AuditLogEntry = { + id: string; + updated_at: string; + changed_by: string; + changed_by_api_key: string; + action: string; + table_name: string; + object_id: string; + before_value: Record; + updated_values: Record; +} + + +const getActionBadge = (action: string) => { + const actionColors = { + created: "blue", + updated: "slate", + deleted: "red", + rotated: "amber", + } as const; + + return ( + + {action} + + + ) +} + +export const auditLogColumns: ColumnDef[] = [ + { + id: "expander", + header: () => null, + cell: ({ row }) => { + const ExpanderCell = () => { + const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded()); + + const toggleHandler = React.useCallback(() => { + setLocalExpanded((prev) => !prev); + row.getToggleExpandedHandler()(); + }, [row]); + + return row.getCanExpand() ? ( + + ) : ( + + ); + }; + return ; + }, + }, + { + header: "Created At", + accessorKey: "before_value.created_at", + cell: (info: any) => { + const createdAt = info.row.original.before_value?.created_at; + return createdAt ? : "-"; + }, + }, + { + header: "Updated At", + accessorKey: "updated_at", + cell: (info: any) => , + }, + { + header: "Action", + accessorKey: "action", + cell: (info: any) => {getActionBadge(info.getValue())}, + }, + { + header: "Changed By", + accessorKey: "changed_by", + cell: (info: any) => { + const changedBy = info.row.original.changed_by; + const apiKey = info.row.original.changed_by_api_key; + return ( +
+
{changedBy}
+ {apiKey && ( // Only show API key if it exists + +
{/* Apply max-width and truncate */} + {apiKey} +
+
+ )} +
+ ); + }, + }, + { + header: "Table Name", + accessorKey: "table_name", + cell: (info: any) => { + const tableName = info.getValue(); + let displayValue = tableName; + switch (tableName) { + case "LiteLLM_VerificationToken": + displayValue = "Keys"; + break; + case "LiteLLM_TeamTable": + displayValue = "Teams"; + break; + case "LiteLLM_OrganizationTable": + displayValue = "Organizations"; + break; + case "LiteLLM_UserTable": + displayValue = "Users"; + break; + default: + displayValue = tableName; + } + return {displayValue}; + }, + }, + { + header: "Affected Item ID", + accessorKey: "object_id", + cell: (props) => { + const ObjectIdDisplay = () => { + const objectId = props.getValue(); + const [copied, setCopied] = useState(false); + + if (!objectId) return <>-; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(String(objectId)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch (err) { + console.error("Failed to copy object ID: ", err); + } + }; + + return ( + + + {String(objectId)} + + + ); + }; + return ; + }, + }, +] \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index b6be331aba..bb0d2a223a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -22,6 +22,16 @@ import FilterComponent from "../common_components/filter"; import { FilterOption } from "../common_components/filter"; import { useLogFilterLogic } from "./log_filter_logic"; import { fetchAllKeyAliases } from "../key_team_helpers/filter_helpers"; +import { + Tab, + TabGroup, + TabList, + TabPanels, + TabPanel, + Text, +} from "@tremor/react"; +import AuditLogs from "./audit_logs"; +import { getTimeRangeDisplay } from "./logs_utils"; interface SpendLogsTableProps { accessToken: string | null; @@ -29,6 +39,7 @@ interface SpendLogsTableProps { userRole: string | null; userID: string | null; allTeams: Team[]; + premiumUser: boolean; } export interface PaginatedResponse { @@ -50,6 +61,7 @@ export default function SpendLogsTable({ userRole, userID, allTeams, + premiumUser, }: SpendLogsTableProps) { const [searchTerm, setSearchTerm] = useState(""); const [showFilters, setShowFilters] = useState(false); @@ -82,6 +94,7 @@ export default function SpendLogsTable({ const [filterByCurrentUser, setFilterByCurrentUser] = useState( userRole && internalUserRoles.includes(userRole) ); + const [activeTab, setActiveTab] = useState("request logs"); const [expandedRequestId, setExpandedRequestId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); @@ -208,7 +221,7 @@ export default function SpendLogsTable({ return response; }, - enabled: !!accessToken && !!token && !!userRole && !!userID, + enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", refetchInterval: 5000, refetchIntervalInBackground: true, }); @@ -354,25 +367,7 @@ export default function SpendLogsTable({ logs.refetch(); }; - // Add this function to format the time range display - const getTimeRangeDisplay = () => { - if (isCustomDate) { - return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; - } - - const now = moment(); - const start = moment(startTime); - const diffMinutes = now.diff(start, 'minutes'); - - if (diffMinutes <= 15) return 'Last 15 Minutes'; - if (diffMinutes <= 60) return 'Last Hour'; - - const diffHours = now.diff(start, 'hours'); - if (diffHours <= 4) return 'Last 4 Hours'; - if (diffHours <= 24) return 'Last 24 Hours'; - if (diffHours <= 168) return 'Last 7 Days'; - return `${start.format('MMM D')} - ${now.format('MMM D')}`; - }; + const handleRowExpand = (requestId: string | null) => { setExpandedRequestId(requestId); @@ -447,252 +442,276 @@ export default function SpendLogsTable({ return (
- -
-

- {selectedSessionId ? ( - <> - Session: {selectedSessionId} - - - ) : ( - "Request Logs" - )} -

-
- {selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( - setSelectedKeyIdInfoView(null)} /> - ) : selectedSessionId ? ( -
- true} - // Optionally: add session-specific row expansion state - /> -
- ) : ( - <> - -
-
-
-
-
- setSearchTerm(e.target.value)} - /> - - - -
- -
-
+ setActiveTab(index === 0 ? "request logs" : "audit logs")}> + + + Request Logs + + + Audit Logs + + + + +
+

+ {selectedSessionId ? ( + <> + Session: {selectedSessionId} - - {quickSelectOpen && ( -
-
- {[ - { label: "Last 15 Minutes", value: 15, unit: "minutes" }, - { label: "Last Hour", value: 1, unit: "hours" }, - { label: "Last 4 Hours", value: 4, unit: "hours" }, - { label: "Last 24 Hours", value: 24, unit: "hours" }, - { label: "Last 7 Days", value: 7, unit: "days" }, - ].map((option) => ( - - ))} -
- -
-
- )} -
- - -

- - {isCustomDate && ( -
-
- { - setStartTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- to -
- { - setEndTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
-
+ + ) : ( + "Request Logs" )} -
- -
- - Showing{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? (currentPage - 1) * pageSize + 1 - : 0}{" "} - -{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? Math.min(currentPage * pageSize, filteredLogs.total) - : 0}{" "} - of{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? filteredLogs.total - : 0}{" "} - results - -
- - Page {logs.isLoading ? "..." : currentPage} of{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? filteredLogs.total_pages - : 1} - - - -
-
+
-
- true} - /> -
- - )} + {selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( + setSelectedKeyIdInfoView(null)} /> + ) : selectedSessionId ? ( +
+ true} + // Optionally: add session-specific row expansion state + /> +
+ ) : ( + <> + +
+
+
+
+
+ setSearchTerm(e.target.value)} + /> + + + +
+ +
+
+ + + {quickSelectOpen && ( +
+
+ {[ + { label: "Last 15 Minutes", value: 15, unit: "minutes" }, + { label: "Last Hour", value: 1, unit: "hours" }, + { label: "Last 4 Hours", value: 4, unit: "hours" }, + { label: "Last 24 Hours", value: 24, unit: "hours" }, + { label: "Last 7 Days", value: 7, unit: "days" }, + ].map((option) => ( + + ))} +
+ +
+
+ )} +
+ + +
+ + {isCustomDate && ( +
+
+ { + setStartTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ to +
+ { + setEndTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ )} +
+ +
+ + Showing{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? (currentPage - 1) * pageSize + 1 + : 0}{" "} + -{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? Math.min(currentPage * pageSize, filteredLogs.total) + : 0}{" "} + of{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? filteredLogs.total + : 0}{" "} + results + +
+ + Page {logs.isLoading ? "..." : currentPage} of{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? filteredLogs.total_pages + : 1} + + + +
+
+
+
+ true} + /> +
+ + )} + + + + + +
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx b/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx new file mode 100644 index 0000000000..b19a5bbead --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx @@ -0,0 +1,21 @@ +import moment from "moment"; + +// Add this function to format the time range display +export const getTimeRangeDisplay = (isCustomDate: boolean, startTime: string, endTime: string) => { + if (isCustomDate) { + return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; + } + + const now = moment(); + const start = moment(startTime); + const diffMinutes = now.diff(start, 'minutes'); + + if (diffMinutes <= 15) return 'Last 15 Minutes'; + if (diffMinutes <= 60) return 'Last Hour'; + + const diffHours = now.diff(start, 'hours'); + if (diffHours <= 4) return 'Last 4 Hours'; + if (diffHours <= 24) return 'Last 24 Hours'; + if (diffHours <= 168) return 'Last 7 Days'; + return `${start.format('MMM D')} - ${now.format('MMM D')}`; +}; \ No newline at end of file