mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
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:
parent
657a60ea5b
commit
326ff422f1
2 changed files with 62 additions and 103 deletions
|
|
@ -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>
|
||||
) : (
|
||||
"—"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue