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>
This commit is contained in:
Mubashir Osmani 2026-09-01 22:02:03 +00:00
parent bad55da9bf
commit 5391ba46c0
5 changed files with 123 additions and 11 deletions

View file

@ -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 (
<Sheet open={open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
@ -189,6 +191,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
<div className="mb-5 rounded-lg border border-border bg-muted p-4">
<p className="mb-2 text-xs font-semibold tracking-wide text-foreground uppercase">Details</p>
<MetadataRow label="Table" value={tableDisplay} />
{objectName != null && <MetadataRow label="Name" value={objectName} />}
<MetadataRow
label="Object ID"
value={

View file

@ -29,6 +29,17 @@ const ROWS: AuditLogEntry[] = [
before_value: { a: 1 },
updated_values: {},
},
{
id: "log-3",
updated_at: "2026-07-20T10:00:00Z",
changed_by: "user-42",
changed_by_api_key: "sk-hash-ghi",
action: "deleted",
table_name: "LiteLLM_ProxyModelTable",
object_id: "model-obj-789",
before_value: { model_name: "gpt-5.6", model_id: "model-obj-789" },
updated_values: {},
},
];
const FIRST_PAGE: PaginationState = { pageIndex: 0, pageSize: 50 };
@ -57,18 +68,25 @@ describe("AuditLogsTable", () => {
// 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();

View file

@ -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 }) => (
<IdentityCell
title={row.original.object_id}
titleClassName="font-mono text-xs font-normal text-primary"
className="max-w-72"
onClick={() => onViewLog(row.original)}
/>
),
cell: ({ row }) => {
const name = getAuditObjectName(row.original);
return (
<IdentityCell
title={name ?? row.original.object_id}
subtitle={name != null ? row.original.object_id : undefined}
titleClassName={name != null ? "text-primary" : "font-mono text-xs font-normal text-primary"}
className="max-w-72"
onClick={() => onViewLog(row.original)}
/>
);
},
},
{
id: "changed_by",

View file

@ -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();
});
});

View file

@ -0,0 +1,21 @@
import type { AuditLogEntry } from "./AuditLogsTableColumns";
const NAME_FIELD_BY_TABLE: Record<string, string> = {
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<string, unknown>)[field];
return typeof name === "string" && name.trim() !== "" ? name : null;
};
export const getAuditObjectName = (log: Pick<AuditLogEntry, "table_name" | "before_value" | "updated_values">) => {
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);
};