From 5391ba46c08cd97b790021c61d29cb7057957fdf Mon Sep 17 00:00:00 2001 From: Mubashir Osmani Date: Tue, 1 Sep 2026 22:02:03 +0000 Subject: [PATCH] feat(ui): show object names in audit logs instead of bare ids Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../AuditLogDrawer/AuditLogDrawer.tsx | 3 + .../view_logs/AuditLogsTable.test.tsx | 22 ++++++- .../view_logs/AuditLogsTableColumns.tsx | 23 ++++--- .../view_logs/auditObjectLabel.test.ts | 65 +++++++++++++++++++ .../components/view_logs/auditObjectLabel.ts | 21 ++++++ 5 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.test.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.ts 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 bf893cbbf09..363b3256905 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -2,6 +2,7 @@ import { Check, Copy } from "lucide-react"; import { useState, useCallback } from "react"; import moment from "moment"; import { AuditLogEntry, AUDIT_TABLE_NAME_DISPLAY } from "../AuditLogsTableColumns"; +import { getAuditObjectName } from "../auditObjectLabel"; import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag"; import CopyButton from "@/components/shared/CopyButton"; import { StatusBadge, type StatusTone } from "@/components/shared/table_cells/status_badge"; @@ -172,6 +173,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { if (!log) return null; const tableDisplay = AUDIT_TABLE_NAME_DISPLAY[log.table_name] ?? log.table_name; + const objectName = getAuditObjectName(log); return ( !nextOpen && onClose()}> @@ -189,6 +191,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {

Details

+ {objectName != null && } { // Action -> StatusBadge with a capitalized label expect(screen.getByText("Created")).toBeInTheDocument(); - expect(screen.getByText("Deleted")).toBeInTheDocument(); + expect(screen.getAllByText("Deleted")).toHaveLength(2); // Table name -> display mapping expect(screen.getByText("Teams")).toBeInTheDocument(); expect(screen.getByText("Users")).toBeInTheDocument(); // Changed By -> DefaultProxyAdminTag (default_user_id becomes a labeled tag; other ids stay raw) expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument(); - expect(screen.getByText("user-42")).toBeInTheDocument(); + expect(screen.getAllByText("user-42")).toHaveLength(2); // Object ID + API key hash expect(screen.getByText("team-obj-123")).toBeInTheDocument(); expect(screen.getByText("sk-hash-abc")).toBeInTheDocument(); }); + it("shows the object name with the id underneath when the audit payload carries one", () => { + renderTable(); + + expect(screen.getByText("gpt-5.6")).toBeInTheDocument(); + expect(screen.getByText("model-obj-789")).toBeInTheDocument(); + }); + it("opens the detail drawer from the Object ID identity cell with the full row", async () => { const user = userEvent.setup(); const props = renderTable(); diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx index 6910ca1c2f7..3c266033c45 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx @@ -5,6 +5,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { DateCell, IdCell, IdentityCell, StatusBadge, type StatusTone } from "@/components/shared/table_cells"; import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; +import { getAuditObjectName } from "./auditObjectLabel"; export type AuditLogEntry = { id: string; @@ -71,17 +72,21 @@ export const getAuditLogsTableColumns = ({ onViewLog }: AuditLogsTableColumnsDep { id: "object_id", accessorKey: "object_id", - header: "Object ID", + header: "Object", minSize: 220, enableSorting: false, - cell: ({ row }) => ( - onViewLog(row.original)} - /> - ), + cell: ({ row }) => { + const name = getAuditObjectName(row.original); + return ( + onViewLog(row.original)} + /> + ); + }, }, { id: "changed_by", diff --git a/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.test.ts b/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.test.ts new file mode 100644 index 00000000000..26b178cd236 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; + +import { getAuditObjectName } from "./auditObjectLabel"; + +describe("getAuditObjectName", () => { + it("reads the model name from before_value for a deleted model", () => { + expect( + getAuditObjectName({ + table_name: "LiteLLM_ProxyModelTable", + before_value: { model_name: "gpt-5.6", model_id: "abc" }, + updated_values: {}, + }), + ).toBe("gpt-5.6"); + }); + + it("prefers updated_values over before_value", () => { + expect( + getAuditObjectName({ + table_name: "LiteLLM_TeamTable", + before_value: { team_alias: "old" }, + updated_values: { team_alias: "new" }, + }), + ).toBe("new"); + }); + + it("maps each table to its human name field", () => { + expect( + getAuditObjectName({ + table_name: "LiteLLM_VerificationToken", + before_value: { key_alias: "k" }, + updated_values: {}, + }), + ).toBe("k"); + expect( + getAuditObjectName({ + table_name: "LiteLLM_UserTable", + before_value: { user_email: "a@b.c" }, + updated_values: {}, + }), + ).toBe("a@b.c"); + expect( + getAuditObjectName({ + table_name: "LiteLLM_OrganizationTable", + before_value: { organization_alias: "org" }, + updated_values: {}, + }), + ).toBe("org"); + }); + + it("returns null for unknown tables, missing or blank names", () => { + expect( + getAuditObjectName({ table_name: "LiteLLM_Config", before_value: { model_name: "x" }, updated_values: {} }), + ).toBeNull(); + expect( + getAuditObjectName({ table_name: "LiteLLM_ProxyModelTable", before_value: {}, updated_values: {} }), + ).toBeNull(); + expect( + getAuditObjectName({ + table_name: "LiteLLM_ProxyModelTable", + before_value: { model_name: " " }, + updated_values: {}, + }), + ).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.ts b/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.ts new file mode 100644 index 00000000000..ee34856a869 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/auditObjectLabel.ts @@ -0,0 +1,21 @@ +import type { AuditLogEntry } from "./AuditLogsTableColumns"; + +const NAME_FIELD_BY_TABLE: Record = { + LiteLLM_ProxyModelTable: "model_name", + LiteLLM_VerificationToken: "key_alias", + LiteLLM_TeamTable: "team_alias", + LiteLLM_UserTable: "user_email", + LiteLLM_OrganizationTable: "organization_alias", +}; + +const readName = (value: unknown, field: string): string | null => { + if (typeof value !== "object" || value === null) return null; + const name = (value as Record)[field]; + return typeof name === "string" && name.trim() !== "" ? name : null; +}; + +export const getAuditObjectName = (log: Pick) => { + const field = NAME_FIELD_BY_TABLE[log.table_name]; + if (field == null) return null; + return readName(log.updated_values, field) ?? readName(log.before_value, field); +};