From 26891c23c5b0442a2875558731f5b0972d80de1f Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 4 Jun 2025 11:42:44 -0700 Subject: [PATCH 1/3] docs: update docs --- docs/my-website/docs/proxy/config_settings.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 03406c9bfa4..0c226ffca64 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -433,6 +433,7 @@ router_settings: | GALILEO_PASSWORD | Password for Galileo authentication | GALILEO_PROJECT_ID | Project ID for Galileo usage | GALILEO_USERNAME | Username for Galileo authentication +| GOOGLE_SECRET_MANAGER_PROJECT_ID | Project ID for Google Secret Manager | GCS_BUCKET_NAME | Name of the Google Cloud Storage bucket | GCS_PATH_SERVICE_ACCOUNT | Path to the Google Cloud service account JSON file | GCS_FLUSH_INTERVAL | Flush interval for GCS logging (in seconds). Specify how often you want a log to be sent to GCS. **Default is 20 seconds** From a24077fb4e6e7793b537b68bc4d19de709e0c78b Mon Sep 17 00:00:00 2001 From: Lik Wei Date: Thu, 5 Jun 2025 03:05:56 +0800 Subject: [PATCH 2/3] fix: add enterprise feature gating to RegenerateKeyModal in KeyInfoView (#11400) Pass premiumUser prop to RegenerateKeyModal to control access Disable Regenerate Key button and show tooltip for non-premium users Ensure consistent enterprise feature messaging across components --- .../src/components/key_info_view.tsx | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_info_view.tsx b/ui/litellm-dashboard/src/components/key_info_view.tsx index 4154c616115..087d60d20ec 100644 --- a/ui/litellm-dashboard/src/components/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/key_info_view.tsx @@ -18,7 +18,7 @@ import { import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; import { keyDeleteCall, keyUpdateCall } from "./networking"; import { KeyResponse } from "./key_team_helpers/key_list"; -import { Form, Input, InputNumber, message, Select } from "antd"; +import { Form, Input, InputNumber, message, Select, Tooltip } from "antd"; import { KeyEditView } from "./key_edit_view"; import { RegenerateKeyModal } from "./regenerate_key_modal"; import { rolesWithWriteAccess } from '../utils/roles'; @@ -34,9 +34,10 @@ interface KeyInfoViewProps { userID: string | null; userRole: string | null; teams: any[] | null; + premiumUser: boolean; } -export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete }: KeyInfoViewProps) { +export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete, premiumUser }: KeyInfoViewProps) { const [isEditing, setIsEditing] = useState(false); const [form] = Form.useForm(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -150,14 +151,19 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user {userRole && rolesWithWriteAccess.includes(userRole) && (
- + + + + + +
+ + + +
+ {/* 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 54670bbab5c..95a38853fad 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 b6be331aba5..bb0d2a223a7 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 00000000000..b19a5bbead3 --- /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