mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
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:
parent
bad55da9bf
commit
5391ba46c0
5 changed files with 123 additions and 11 deletions
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue