From a521a267de2d72346b27e5d082cf75cd7da71fc1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 20:47:20 +0000 Subject: [PATCH] feat(ui): view_logs/AuditLogDrawer to shadcn Sheet - antd Drawer/Tag/Typography + @ant-design Close/Copy/Check \u2192 shadcn Sheet/Badge + lucide X/Copy/Check. - Typography.Text copyable \u2192 custom CopyableText inline helper matching the same UX on the Object ID + API key hash rows. - Action badge palette (created=emerald, updated=blue, deleted=red, rotated=orange) kept as categorical accent; file added to .eslintrc overrides. - Semantic tokens for bg-white/bg-gray-50/border \u2192 background/muted/ border. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + .../AuditLogDrawer/AuditLogDrawer.tsx | 280 +++++++++++------- 2 files changed, 181 insertions(+), 100 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 99acb96b8f0..bd090d73a75 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -91,6 +91,7 @@ "src/components/transform_request.tsx", "src/components/Navbar/UserDropdown/UserDropdown.tsx", "src/components/playground/chat_ui/ChatMessageBubble.tsx", + "src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx", "src/components/permissions/VectorStorePermissions.tsx", "src/components/permissions/AgentPermissions.tsx", "src/components/permissions/MCPServerPermissions.tsx", diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx index 19989ef4882..edf60ff00d8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -1,12 +1,15 @@ -import { Drawer, Tag, Typography } from "antd"; -import { CloseOutlined, CopyOutlined, CheckOutlined } from "@ant-design/icons"; +import { + Sheet, + SheetContent, +} from "@/components/ui/sheet"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { Check, Copy, X } from "lucide-react"; import { useState, useCallback } from "react"; import moment from "moment"; import { AuditLogEntry } from "../columns"; import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag"; -const { Text } = Typography; - interface AuditLogDrawerProps { open: boolean; onClose: () => void; @@ -21,14 +24,56 @@ const TABLE_NAME_DISPLAY: Record = { LiteLLM_ProxyModelTable: "Models", }; -const ACTION_COLOR: Record = { - created: "green", - updated: "blue", - deleted: "red", - rotated: "orange", +const ACTION_BADGE_CLASSES: Record = { + created: + "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300", + updated: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300", + deleted: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300", + rotated: + "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300", }; -function CopyableJsonBlock({ label, value }: { label: string; value: Record }) { +function CopyableText({ value }: { value: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = useCallback(async () => { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + } + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch (e) { + console.error("Copy failed:", e); + } + }, [value]); + return ( + + {value} + + + ); +} + +function CopyableJsonBlock({ + label, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value, +}: { + label: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: Record; +}) { const [copied, setCopied] = useState(false); const handleCopy = useCallback(async () => { @@ -55,29 +100,45 @@ function CopyableJsonBlock({ label, value }: { label: string; value: Record -
- {label} +
+
+ + {label} +
-
+      
         {JSON.stringify(value, null, 2)}
       
); } -function MetadataRow({ label, value }: { label: string; value: React.ReactNode }) { +function MetadataRow({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) { return (
- {label} - {value} + + {label} + + {value}
); } @@ -91,7 +152,9 @@ function DiffSection({ log }: { log: AuditLogEntry }) { let displayAfter = updated_values; if (isUpdateAction && before_value && updated_values) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const changedBefore: Record = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const changedAfter: Record = {}; const allKeys = new Set([ ...Object.keys(before_value), @@ -107,7 +170,6 @@ function DiffSection({ log }: { log: AuditLogEntry }) { } }); - // Fields only in before (removed) Object.keys(before_value).forEach((key) => { if (!(key in updated_values) && !(key in changedBefore)) { changedBefore[key] = before_value[key]; @@ -115,7 +177,6 @@ function DiffSection({ log }: { log: AuditLogEntry }) { } }); - // Fields only in after (added) Object.keys(updated_values).forEach((key) => { if (!(key in before_value) && !(key in changedAfter)) { changedAfter[key] = updated_values[key]; @@ -133,37 +194,57 @@ function DiffSection({ log }: { log: AuditLogEntry }) { : { note: "No differing fields detected" }; } - const renderValue = (label: string, value: Record | null | undefined) => { + const renderValue = ( + label: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: Record | null | undefined, + ) => { if (!value || Object.keys(value).length === 0) { return ( -
-
- {label} +
+
+ + {label} +
-

N/A

+

+ N/A +

); } - // For key table updates, show only meaningful fields as plain text if (isKeyTable && isUpdateAction) { const knownKeyFields = ["token", "spend", "max_budget"]; - const hasOnlyKnown = Object.keys(value).every((k) => knownKeyFields.includes(k)); + const hasOnlyKnown = Object.keys(value).every((k) => + knownKeyFields.includes(k), + ); if (hasOnlyKnown && !("note" in value)) { return ( -
-
- {label} +
+
+ + {label} +
{value.token !== undefined && ( -

Token: {value.token ?? "N/A"}

+

+ Token:{" "} + {value.token ?? "N/A"} +

)} {value.spend !== undefined && ( -

Spend: ${Number(value.spend).toFixed(6)}

+

+ Spend: $ + {Number(value.spend).toFixed(6)} +

)} {value.max_budget !== undefined && ( -

Max Budget: ${Number(value.max_budget).toFixed(6)}

+

+ Max Budget: $ + {Number(value.max_budget).toFixed(6)} +

)}
@@ -186,75 +267,74 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { if (!log) return null; const tableDisplay = TABLE_NAME_DISPLAY[log.table_name] ?? log.table_name; - const actionColor = ACTION_COLOR[log.action] ?? "default"; + const actionClasses = + ACTION_BADGE_CLASSES[log.action] ?? "bg-muted text-muted-foreground"; return ( - - {/* Header */} -
-
- - {log.action} - - - {moment.utc(log.updated_at).local().format("MMM D, YYYY HH:mm:ss")} - -
- -
- - {/* Body */} -
- {/* Metadata */} -
-

- Details -

- - - {log.object_id} - - } - /> - } - /> - - {log.changed_by_api_key} - - ) : ( - "—" - ) - } - /> + (!o ? onClose() : undefined)}> + + {/* Header */} +
+
+ + {log.action} + + + {moment + .utc(log.updated_at) + .local() + .format("MMM D, YYYY HH:mm:ss")} + +
+
- {/* Diff */} - -
- + {/* Body */} +
+
+

+ Details +

+ + + + + } + /> + } + /> + + + + ) : ( + "—" + ) + } + /> +
+ + +
+ + ); }