fix(ui): Audit logs table and drawer polish

Table:
- Use DefaultProxyAdminTag for changed_by column
- Remove tooltips on Object ID and API Key columns
- Rename API Key column to API Key (Hash)
- Move pagination controls to upper-right of filter bar; add icon-only refresh button

Drawer:
- Object ID is now copyable
- API Key (Hash) is copyable and no longer truncated
- Changed By uses DefaultProxyAdminTag
- Expand JSON view boxes from max-h-72 to max-h-96
- Remove unnecessary vertical scrollbar (drop overflow-auto h-full from body div;
  use flex column layout so header and content flow naturally)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-03 15:54:46 -08:00
parent 657a60ea5b
commit 326ff422f1
2 changed files with 62 additions and 103 deletions

View file

@ -1,7 +1,10 @@
import { Drawer, Tag, Tooltip } from "antd";
import { Drawer, Tag, Typography } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import moment from "moment";
import { AuditLogEntry } from "../columns";
import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag";
const { Text } = Typography;
interface AuditLogDrawerProps {
open: boolean;
@ -26,7 +29,7 @@ const ACTION_COLOR: Record<string, string> = {
function JsonBlock({ value }: { value: Record<string, any> }) {
return (
<pre className="p-3 bg-gray-50 border rounded text-xs font-mono overflow-auto max-h-72 whitespace-pre-wrap break-all">
<pre className="p-3 bg-gray-50 border rounded text-xs font-mono overflow-auto max-h-96 whitespace-pre-wrap break-all">
{JSON.stringify(value, null, 2)}
</pre>
);
@ -92,7 +95,7 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
: { note: "No differing fields detected" };
}
const renderValue = (value: Record<string, any> | null | undefined, label: string) => {
const renderValue = (value: Record<string, any> | null | undefined) => {
if (!value || Object.keys(value).length === 0) {
return <p className="text-xs text-gray-400 italic">N/A</p>;
}
@ -125,11 +128,11 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<p className="text-xs font-semibold text-gray-600 mb-2">Before</p>
{renderValue(displayBefore, "before")}
{renderValue(displayBefore)}
</div>
<div>
<p className="text-xs font-semibold text-gray-600 mb-2">After</p>
{renderValue(displayAfter, "after")}
{renderValue(displayAfter)}
</div>
</div>
);
@ -150,10 +153,10 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
closable={false}
mask={true}
maskClosable={true}
styles={{ body: { padding: 0 }, header: { display: "none" } }}
styles={{ body: { padding: 0, display: "flex", flexDirection: "column" }, header: { display: "none" } }}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b bg-white sticky top-0 z-10">
<div className="flex items-center justify-between px-6 py-4 border-b bg-white shrink-0">
<div className="flex items-center gap-3">
<Tag color={actionColor} className="capitalize m-0">
{log.action}
@ -172,7 +175,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
</div>
{/* Body */}
<div className="px-6 py-5 overflow-auto h-full">
<div className="px-6 py-5">
{/* Metadata */}
<div className="bg-gray-50 border rounded-lg p-4 mb-5">
<p className="text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wide">
@ -182,21 +185,22 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
<MetadataRow
label="Object ID"
value={
<Tooltip title={log.object_id}>
<span className="font-mono">{log.object_id}</span>
</Tooltip>
<Text copyable className="font-mono text-xs">
{log.object_id}
</Text>
}
/>
<MetadataRow label="Changed By" value={log.changed_by || "—"} />
<MetadataRow
label="API Key"
label="Changed By"
value={<DefaultProxyAdminTag userId={log.changed_by} />}
/>
<MetadataRow
label="API Key (Hash)"
value={
log.changed_by_api_key ? (
<Tooltip title={log.changed_by_api_key}>
<span className="font-mono">
{log.changed_by_api_key.slice(0, 12)}…
</span>
</Tooltip>
<Text copyable className="font-mono text-xs break-all">
{log.changed_by_api_key}
</Text>
) : (
"—"
)

View file

@ -1,12 +1,13 @@
import { useState } from "react";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { Table, Tag, Input, Select, Button, Tooltip } from "antd";
import { Table, Tag, Input, Select, Button, Pagination } from "antd";
import { ReloadOutlined } from "@ant-design/icons";
import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
import type { ColumnsType } from "antd/es/table";
import moment from "moment";
import { uiAuditLogsCall } from "../networking";
import { AuditLogEntry } from "./columns";
import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
const { Search } = Input;
@ -97,14 +98,7 @@ export default function AuditLogs({
placeholderData: keepPreviousData,
});
const handleFilterChange = () => {
// Reset to page 1 whenever a filter changes
setPage(1);
};
const handleTableChange = (pagination: TablePaginationConfig) => {
setPage(pagination.current ?? 1);
};
const resetPage = () => setPage(1);
const handleRowClick = (log: AuditLogEntry) => {
setSelectedLog(log);
@ -146,9 +140,7 @@ export default function AuditLogs({
dataIndex: "object_id",
key: "object_id",
render: (val: string) => (
<Tooltip title={val}>
<span className="font-mono text-xs">{val}</span>
</Tooltip>
<span className="font-mono text-xs">{val}</span>
),
},
{
@ -156,18 +148,16 @@ export default function AuditLogs({
dataIndex: "changed_by",
key: "changed_by",
width: 200,
render: (val: string) => val || "—",
render: (val: string) => <DefaultProxyAdminTag userId={val} />,
},
{
title: "API Key",
title: "API Key (Hash)",
dataIndex: "changed_by_api_key",
key: "changed_by_api_key",
width: 140,
render: (val: string) =>
val ? (
<Tooltip title={val}>
<span className="font-mono text-xs">{val.slice(0, 12)}…</span>
</Tooltip>
<span className="font-mono text-xs">{val.slice(0, 12)}…</span>
) : (
"—"
),
@ -212,76 +202,37 @@ export default function AuditLogs({
<div className="border-b px-6 py-4">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">Audit Logs</h1>
<Button
icon={<ReloadOutlined spin={query.isFetching} />}
onClick={() => query.refetch()}
disabled={query.isFetching}
>
Refresh
</Button>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-3">
{/* Filters + pagination on same row */}
<div className="flex flex-wrap items-center gap-3">
<Search
placeholder="Object ID"
allowClear
style={{ width: 200 }}
onSearch={(val) => {
setObjectId(val);
handleFilterChange();
}}
onChange={(e) => {
if (!e.target.value) {
setObjectId("");
handleFilterChange();
}
}}
onSearch={(val) => { setObjectId(val); resetPage(); }}
onChange={(e) => { if (!e.target.value) { setObjectId(""); resetPage(); } }}
/>
<Search
placeholder="Changed By"
allowClear
style={{ width: 180 }}
onSearch={(val) => {
setChangedBy(val);
handleFilterChange();
}}
onChange={(e) => {
if (!e.target.value) {
setChangedBy("");
handleFilterChange();
}
}}
onSearch={(val) => { setChangedBy(val); resetPage(); }}
onChange={(e) => { if (!e.target.value) { setChangedBy(""); resetPage(); } }}
/>
<Search
placeholder="Team ID"
allowClear
style={{ width: 180 }}
onSearch={(val) => {
setTeamId(val);
handleFilterChange();
}}
onChange={(e) => {
if (!e.target.value) {
setTeamId("");
handleFilterChange();
}
}}
onSearch={(val) => { setTeamId(val); resetPage(); }}
onChange={(e) => { if (!e.target.value) { setTeamId(""); resetPage(); } }}
/>
<Search
placeholder="Key Hash"
allowClear
style={{ width: 180 }}
onSearch={(val) => {
setKeyHash(val);
handleFilterChange();
}}
onChange={(e) => {
if (!e.target.value) {
setKeyHash("");
handleFilterChange();
}
}}
onSearch={(val) => { setKeyHash(val); resetPage(); }}
onChange={(e) => { if (!e.target.value) { setKeyHash(""); resetPage(); } }}
/>
<Select
placeholder="All Actions"
@ -293,10 +244,7 @@ export default function AuditLogs({
{ label: "Deleted", value: "deleted" },
{ label: "Rotated", value: "rotated" },
]}
onChange={(val) => {
setAction(val);
handleFilterChange();
}}
onChange={(val) => { setAction(val); resetPage(); }}
/>
<Select
placeholder="All Tables"
@ -309,34 +257,41 @@ export default function AuditLogs({
{ label: "Organizations", value: "LiteLLM_OrganizationTable" },
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
]}
onChange={(val) => {
setTableName(val);
handleFilterChange();
}}
onChange={(val) => { setTableName(val); resetPage(); }}
/>
{/* Pagination + refresh pushed to the right */}
<div className="ml-auto flex items-center gap-2">
<Button
icon={<ReloadOutlined spin={query.isFetching} />}
onClick={() => query.refetch()}
disabled={query.isFetching}
/>
<Pagination
current={page}
pageSize={PAGE_SIZE}
total={total}
showTotal={(t) => `${t} total`}
showSizeChanger={false}
size="small"
onChange={(p) => setPage(p)}
/>
</div>
</div>
</div>
{/* Table */}
{/* Table — pagination handled in header */}
<Table<AuditLogEntry>
columns={columns}
dataSource={auditLogs}
rowKey="id"
loading={query.isLoading}
size="small"
pagination={false}
onRow={(record) => ({
onClick: () => handleRowClick(record),
style: { cursor: "pointer" },
})}
pagination={{
current: page,
pageSize: PAGE_SIZE,
total,
showTotal: (t) => `${t} total`,
showSizeChanger: false,
onChange: (p) => setPage(p),
}}
onChange={handleTableChange}
/>
</div>