diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx index 5fcc55c1e98..e11a2b03359 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { NuqsTestingAdapter } from "nuqs/adapters/testing"; import React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -77,11 +78,13 @@ const defaultProps = { teams: [], }; -const renderDashboard = () => +const renderDashboard = (searchParams?: Record) => render( - - - , + + + + + , ); describe("ViewUserDashboard", () => { @@ -142,6 +145,12 @@ describe("ViewUserDashboard", () => { expect(screen.queryByText("test@example.com")).not.toBeInTheDocument(); }); + it("should open the detail view directly from a ?user= deep link", async () => { + renderDashboard({ user: "user-1" }); + + expect(await screen.findByTestId("user-info-view")).toHaveTextContent("detail:user-1:false"); + }); + it("should open the detail view in edit mode from the row actions menu", async () => { const user = userEvent.setup(); renderDashboard(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx index 2c1d28d82f8..f100a906f96 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx @@ -1,4 +1,5 @@ import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; +import { parseAsString, useQueryState } from "nuqs"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "antd"; @@ -72,7 +73,7 @@ const ViewUserDashboard: React.FC = ({ const [selectionMode, setSelectionMode] = useState(false); const [isBulkEditModalVisible, setIsBulkEditModalVisible] = useState(false); - const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedUserId, setSelectedUserId] = useQueryState("user", parseAsString.withOptions({ history: "push" })); const [openInEditMode, setOpenInEditMode] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); @@ -139,15 +140,18 @@ const ViewUserDashboard: React.FC = ({ setRowSelection({}); }, []); - const handleUserClick = useCallback((userId: string, openInEdit: boolean = false) => { - setSelectedUserId(userId); - setOpenInEditMode(openInEdit); - }, []); + const handleUserClick = useCallback( + (userId: string, openInEdit: boolean = false) => { + setSelectedUserId(userId); + setOpenInEditMode(openInEdit); + }, + [setSelectedUserId], + ); const handleCloseUserInfo = useCallback(() => { setSelectedUserId(null); setOpenInEditMode(false); - }, []); + }, [setSelectedUserId]); const handleDelete = useCallback((user: UserInfo) => { setUserToDelete(user); diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 8e91f7feecc..a00b3bb74b1 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -6447,7 +6447,10 @@ interface UiAuditLogsParams { changed_by?: string; changed_by_api_key?: string; object_team_id?: string; + object_team?: string; object_key_hash?: string; + start_date?: string; + end_date?: string; sort_by?: string; sort_order?: "asc" | "desc"; } diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.test.tsx new file mode 100644 index 00000000000..ac57b195661 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import type { AuditLogEntry } from "../AuditLogsTableColumns"; +import { AuditLogDrawer } from "./AuditLogDrawer"; + +const BASE_LOG: AuditLogEntry = { + id: "log-1", + updated_at: "2026-07-20T12:00:00Z", + changed_by: "user-42", + changed_by_api_key: "sk-hash-abc", + action: "updated", + table_name: "LiteLLM_TeamTable", + object_id: "team-obj-123", + before_value: { spend: 1 }, + updated_values: { spend: 2 }, +}; + +const ENRICHED_LOG: AuditLogEntry = { + ...BASE_LOG, + object_alias: "prod-team", + changed_by_user_email: "admin@example.com", + changed_by_key_alias: "admin-key", +}; + +function renderDrawer(log: AuditLogEntry) { + render(); +} + +describe("AuditLogDrawer", () => { + it("shows the alias, changed-by email, and key alias alongside the raw id and hash", () => { + renderDrawer(ENRICHED_LOG); + + expect(screen.getByText("prod-team")).toBeInTheDocument(); + expect(screen.getByText("admin@example.com")).toBeInTheDocument(); + expect(screen.getByText("user-42")).toBeInTheDocument(); + expect(screen.getByText("admin-key")).toBeInTheDocument(); + expect(screen.getByText("sk-hash-abc")).toBeInTheDocument(); + }); + + it("falls back gracefully when the enrichment fields are absent", () => { + renderDrawer(BASE_LOG); + + expect(screen.getByText("—")).toBeInTheDocument(); + expect(screen.getByText("user-42")).toBeInTheDocument(); + expect(screen.getByText("sk-hash-abc")).toBeInTheDocument(); + expect(screen.queryByText("admin@example.com")).toBeNull(); + }); +}); 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 81759c80ab6..36cd7f39453 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -229,14 +229,26 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { } /> - } /> + + + {!!log.changed_by_user_email && {log.changed_by_user_email}} + + + } + /> - {log.changed_by_api_key} - + + {!!log.changed_by_key_alias && {log.changed_by_key_alias}} + + {log.changed_by_api_key} + + ) : ( "—" ) diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.test.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.test.tsx new file mode 100644 index 00000000000..7c1fd2d288f --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.test.tsx @@ -0,0 +1,103 @@ +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import moment from "moment"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders, testQueryClient } from "../../../tests/test-utils"; +import AuditLogsPanel from "./AuditLogsPanel"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn() }), +})); + +const uiAuditLogsCall = vi.fn(); + +vi.mock("../networking", () => ({ + uiAuditLogsCall: (...args: unknown[]) => uiAuditLogsCall(...args), + serverRootPath: "", +})); + +const EMPTY_RESPONSE = { audit_logs: [], total: 0, page: 1, page_size: 50, total_pages: 0 }; + +function renderPanel() { + return renderWithProviders( + , + ); +} + +async function applyFilter(fill: () => Promise | void) { + const user = userEvent.setup(); + await user.click(screen.getByTestId("datatable-filters-trigger")); + await fill(); + await user.click(screen.getByTestId("filter-drawer-apply")); +} + +function lastCallParams(): Record { + const lastCall = uiAuditLogsCall.mock.calls.at(-1)?.[0] as { params: Record }; + return lastCall.params; +} + +describe("AuditLogsPanel", () => { + beforeEach(() => { + vi.clearAllMocks(); + testQueryClient.clear(); + uiAuditLogsCall.mockResolvedValue(EMPTY_RESPONSE); + }); + + it("sends the team filter as the object_team query param", async () => { + renderPanel(); + await waitFor(() => expect(uiAuditLogsCall).toHaveBeenCalled()); + + const user = userEvent.setup(); + await applyFilter(async () => { + await user.type(await screen.findByPlaceholderText("Team ID or alias…"), "ml-platform"); + }); + + await waitFor(() => { + expect(lastCallParams().object_team).toBe("ml-platform"); + }); + expect(lastCallParams().object_team_id).toBeUndefined(); + }); + + it("sends the date range as UTC start_date and end_date query params", async () => { + renderPanel(); + await waitFor(() => expect(uiAuditLogsCall).toHaveBeenCalled()); + + await applyFilter(async () => { + fireEvent.change(await screen.findByTestId("audit-filter-start-date"), { + target: { value: "2026-07-01T10:00" }, + }); + fireEvent.change(screen.getByTestId("audit-filter-end-date"), { target: { value: "2026-07-02T18:30" } }); + }); + + await waitFor(() => { + expect(lastCallParams().start_date).toBe(moment("2026-07-01T10:00").utc().format("YYYY-MM-DD HH:mm:ss")); + }); + expect(lastCallParams().end_date).toBe(moment("2026-07-02T18:30").utc().format("YYYY-MM-DD HH:mm:ss")); + }); + + it("keeps the pre-existing filters mapped to their unchanged query params", async () => { + renderPanel(); + await waitFor(() => expect(uiAuditLogsCall).toHaveBeenCalled()); + + const user = userEvent.setup(); + await applyFilter(async () => { + await user.type(await screen.findByPlaceholderText("Enter object ID…"), "obj-1"); + await user.type(screen.getByPlaceholderText("Enter user ID…"), "changer-1"); + await user.type(screen.getByPlaceholderText("Enter key hash…"), "hash-1"); + }); + + await waitFor(() => { + expect(lastCallParams().object_id).toBe("obj-1"); + }); + expect(lastCallParams().changed_by).toBe("changer-1"); + expect(lastCallParams().object_key_hash).toBe("hash-1"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx index 81bd4a19f76..e19192b6348 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx @@ -1,6 +1,7 @@ import { useCallback, useState } from "react"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table"; +import moment from "moment"; import { resolveLogoSrc } from "@/lib/assetPaths"; import { uiAuditLogsCall } from "../networking"; import { AuditLogEntry } from "./AuditLogsTableColumns"; @@ -21,6 +22,9 @@ const auditLogsPreviewImg = `${asset_logos_folder}audit-logs-preview.png`; const PAGE_SIZE = 50; +const toUtcTimestamp = (value: string | undefined): string | undefined => + value === undefined ? undefined : moment(value).utc().format("YYYY-MM-DD HH:mm:ss"); + interface AuditLogsResponse { audit_logs: AuditLogEntry[]; total: number; @@ -63,9 +67,11 @@ export default function AuditLogsPanel({ object_id: getFilterValue("object_id"), changed_by: getFilterValue("changed_by"), object_key_hash: getFilterValue("key_hash"), - object_team_id: getFilterValue("team_id"), + object_team: getFilterValue("object_team"), action: getFilterValue("action"), table_name: getFilterValue("table_name"), + start_date: toUtcTimestamp(getFilterValue("start_date")), + end_date: toUtcTimestamp(getFilterValue("end_date")), sort_by: "updated_at", sort_order: "desc", }, diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx index dbb0a39e2ee..3c52fdabfb0 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx @@ -1,11 +1,19 @@ import type { ColumnFiltersState, PaginationState } from "@tanstack/react-table"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { AuditLogsTable } from "./AuditLogsTable"; import type { AuditLogEntry } from "./AuditLogsTableColumns"; +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn() }), +})); + +vi.mock("../networking", () => ({ + serverRootPath: "", +})); + const ROWS: AuditLogEntry[] = [ { id: "log-1", @@ -17,6 +25,9 @@ const ROWS: AuditLogEntry[] = [ object_id: "team-obj-123", before_value: {}, updated_values: { foo: "bar" }, + object_alias: "prod-team", + changed_by_user_email: null, + changed_by_key_alias: null, }, { id: "log-2", @@ -28,9 +39,21 @@ const ROWS: AuditLogEntry[] = [ object_id: "user-obj-456", before_value: { a: 1 }, updated_values: {}, + changed_by_user_email: "admin@example.com", + changed_by_key_alias: "admin-key", }, ]; +const makeRow = (overrides: Partial & Pick) => ({ + updated_at: "2026-07-20T10:00:00Z", + changed_by: "user-42", + changed_by_api_key: "sk-hash-xyz", + action: "updated", + before_value: {}, + updated_values: {}, + ...overrides, +}); + const FIRST_PAGE: PaginationState = { pageIndex: 0, pageSize: 50 }; function renderTable(overrides: Partial> = {}) { @@ -69,16 +92,81 @@ describe("AuditLogsTable", () => { expect(screen.getByText("sk-hash-abc")).toBeInTheDocument(); }); - it("opens the detail drawer from the Object ID identity cell with the full row", async () => { + it("renders the alias column with the object alias and an em-dash fallback when absent", () => { + renderTable(); + + expect(screen.getByText("prod-team")).toBeInTheDocument(); + expect(screen.getAllByText("—").length).toBeGreaterThan(0); + }); + + it("shows the changed-by email and key alias while keeping the raw id and hash visible", () => { + renderTable(); + + expect(screen.getByText("admin@example.com")).toBeInTheDocument(); + expect(screen.getByText("user-42")).toBeInTheDocument(); + expect(screen.getByText("admin-key")).toBeInTheDocument(); + expect(screen.getByText("sk-hash-def")).toBeInTheDocument(); + }); + + it("links object ids and changed-by users to their entity detail pages", () => { + renderTable({ + data: [ + ...ROWS, + makeRow({ id: "log-3", table_name: "LiteLLM_VerificationToken", object_id: "keyhash-1" }), + makeRow({ id: "log-4", table_name: "LiteLLM_OrganizationTable", object_id: "org-1" }), + makeRow({ id: "log-5", table_name: "LiteLLM_ProxyModelTable", object_id: "model-1" }), + makeRow({ id: "log-6", table_name: "SomeUnknownTable", object_id: "mystery-1" }), + ], + rowCount: 6, + }); + + expect(screen.getByRole("link", { name: "team-obj-123" })).toHaveAttribute( + "href", + expect.stringContaining("/teams?team=team-obj-123"), + ); + expect(screen.getByRole("link", { name: "user-obj-456" })).toHaveAttribute( + "href", + expect.stringContaining("/users?user=user-obj-456"), + ); + expect(screen.getByRole("link", { name: "keyhash-1" })).toHaveAttribute( + "href", + expect.stringContaining("/api-keys?key=keyhash-1"), + ); + expect(screen.getByRole("link", { name: "org-1" })).toHaveAttribute( + "href", + expect.stringContaining("/organizations?org=org-1"), + ); + expect(screen.getByRole("link", { name: "model-1" })).toHaveAttribute( + "href", + expect.stringContaining("/models-and-endpoints?model=model-1"), + ); + expect(screen.getAllByRole("link", { name: "admin@example.com" })[0]).toHaveAttribute( + "href", + expect.stringContaining("/users?user=user-42"), + ); + expect(screen.queryByRole("link", { name: "mystery-1" })).toBeNull(); + expect(screen.getByText("mystery-1")).toBeInTheDocument(); + }); + + it("opens the detail drawer from a row click with the full row", async () => { const user = userEvent.setup(); const props = renderTable(); - await user.click(screen.getByText("team-obj-123")); + await user.click(screen.getByText("Teams")); expect(props.onViewLog).toHaveBeenCalledTimes(1); expect(props.onViewLog).toHaveBeenCalledWith(ROWS[0]); }); + it("does not open the drawer when the object id entity link is clicked", async () => { + const user = userEvent.setup(); + const props = renderTable(); + + await user.click(screen.getByRole("link", { name: "team-obj-123" })); + + expect(props.onViewLog).not.toHaveBeenCalled(); + }); + it("drives the shared footer from the server rowCount and reports page changes", async () => { const user = userEvent.setup(); const onPaginationChange = vi.fn(); @@ -143,4 +231,38 @@ describe("AuditLogsTable", () => { const committed = typeof arg === "function" ? arg([]) : arg; expect(committed).toEqual([{ id: "object_id", value: "obj-9" }]); }); + + it("commits the team filter as an object_team filter accepting id or alias", async () => { + const user = userEvent.setup(); + const onColumnFiltersChange = vi.fn(); + renderTable({ onColumnFiltersChange }); + + await user.click(screen.getByTestId("datatable-filters-trigger")); + await user.type(await screen.findByPlaceholderText("Team ID or alias…"), "ml-platform"); + await user.click(screen.getByTestId("filter-drawer-apply")); + + const arg = onColumnFiltersChange.mock.calls[0][0]; + const committed = typeof arg === "function" ? arg([]) : arg; + expect(committed).toEqual([{ id: "object_team", value: "ml-platform" }]); + }); + + it("commits the date range as start_date and end_date filters", async () => { + const user = userEvent.setup(); + const onColumnFiltersChange = vi.fn(); + renderTable({ onColumnFiltersChange }); + + await user.click(screen.getByTestId("datatable-filters-trigger")); + fireEvent.change(await screen.findByTestId("audit-filter-start-date"), { target: { value: "2026-07-01T10:00" } }); + fireEvent.change(screen.getByTestId("audit-filter-end-date"), { target: { value: "2026-07-02T18:30" } }); + await user.click(screen.getByTestId("filter-drawer-apply")); + + const arg = onColumnFiltersChange.mock.calls[0][0]; + const committed = typeof arg === "function" ? arg([]) : arg; + expect(committed).toEqual( + expect.arrayContaining([ + { id: "start_date", value: "2026-07-01T10:00" }, + { id: "end_date", value: "2026-07-02T18:30" }, + ]), + ); + }); }); diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx index bcdce12fce8..81d5031a513 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx @@ -2,6 +2,7 @@ import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table"; import { ScrollText } from "lucide-react"; +import moment from "moment"; import { useMemo, useState } from "react"; import { @@ -46,9 +47,11 @@ const TABLE_OPTIONS = [ ] as const; const FILTER_LABELS: Record = { + start_date: "Start Date", + end_date: "End Date", object_id: "Object ID", changed_by: "Changed By", - team_id: "Team ID", + object_team: "Team", key_hash: "Key Hash", action: "Action", table_name: "Table", @@ -62,6 +65,9 @@ const formatFilterValue = (columnId: string, value: unknown): string => { if (columnId === "table_name") { return AUDIT_TABLE_NAME_DISPLAY[raw] ?? raw; } + if (columnId === "start_date" || columnId === "end_date") { + return moment(raw).format("MMM D, YYYY HH:mm"); + } return raw; }; @@ -96,7 +102,7 @@ export function AuditLogsTable({ onViewLog, }: AuditLogsTableProps) { const [filtersOpen, setFiltersOpen] = useState(false); - const columns = useMemo(() => getAuditLogsTableColumns({ onViewLog }), [onViewLog]); + const columns = useMemo(() => getAuditLogsTableColumns(), []); return ( 0} />} size="compact" + onRowClick={onViewLog} toolbar={(table) => ( <> {({ get, set }) => ( <> + +
+ set("start_date", event.target.value)} + /> + to + set("end_date", event.target.value)} + /> +
+
- + set("team_id", event.target.value)} - placeholder="Enter team ID…" + value={(get("object_team") as string) ?? ""} + onChange={(event) => set("object_team", event.target.value)} + placeholder="Team ID or alias…" /> diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx index 6910ca1c2f7..688a8340e70 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx @@ -2,7 +2,9 @@ import { ColumnDef } from "@tanstack/react-table"; -import { DateCell, IdCell, IdentityCell, StatusBadge, type StatusTone } from "@/components/shared/table_cells"; +import { BadgeLink } from "@/components/shared/BadgeLink"; +import { DateCell, IdCell, StatusBadge, type StatusTone } from "@/components/shared/table_cells"; +import { keyDetailHref, modelDetailHref, orgDetailHref, teamDetailHref, userDetailHref } from "@/utils/entityLinks"; import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; @@ -16,6 +18,9 @@ export type AuditLogEntry = { object_id: string; before_value: Record; updated_values: Record; + object_alias?: string | null; + changed_by_user_email?: string | null; + changed_by_key_alias?: string | null; }; export const AUDIT_TABLE_NAME_DISPLAY: Record = { @@ -26,6 +31,17 @@ export const AUDIT_TABLE_NAME_DISPLAY: Record = { LiteLLM_ProxyModelTable: "Models", }; +const OBJECT_HREF_BY_TABLE: Record string> = { + LiteLLM_VerificationToken: keyDetailHref, + LiteLLM_TeamTable: teamDetailHref, + LiteLLM_UserTable: userDetailHref, + LiteLLM_OrganizationTable: orgDetailHref, + LiteLLM_ProxyModelTable: modelDetailHref, +}; + +const auditObjectDetailHref = (tableName: string, objectId: string): string | undefined => + objectId ? OBJECT_HREF_BY_TABLE[tableName]?.(objectId) : undefined; + const ACTION_TONE: Record = { created: "success", updated: "info", @@ -33,13 +49,45 @@ const ACTION_TONE: Record = { rotated: "warning", }; +const DEFAULT_PROXY_ADMIN_USER_ID = "default_user_id"; + const capitalize = (value: string): string => (value ? value.charAt(0).toUpperCase() + value.slice(1) : value); -interface AuditLogsTableColumnsDeps { - onViewLog: (log: AuditLogEntry) => void; +function ChangedByCell({ userId, userEmail }: { userId: string; userEmail: string | null | undefined }) { + if (!userId || userId === DEFAULT_PROXY_ADMIN_USER_ID) { + return ; + } + + return ( +
+ + {userEmail || userId} + + {!!userEmail && ( + + {userId} + + )} +
+ ); } -export const getAuditLogsTableColumns = ({ onViewLog }: AuditLogsTableColumnsDeps): ColumnDef[] => [ +function ChangedByApiKeyCell({ keyHash, keyAlias }: { keyHash: string; keyAlias: string | null | undefined }) { + if (!keyAlias) { + return ; + } + + return ( +
+ + {keyAlias} + + +
+ ); +} + +export const getAuditLogsTableColumns = (): ColumnDef[] => [ { id: "updated_at", accessorKey: "updated_at", @@ -75,21 +123,38 @@ export const getAuditLogsTableColumns = ({ onViewLog }: AuditLogsTableColumnsDep minSize: 220, enableSorting: false, cell: ({ row }) => ( - onViewLog(row.original)} - /> + + {row.original.object_id} + ), }, + { + id: "object_alias", + accessorKey: "object_alias", + header: "Alias", + size: 160, + enableSorting: false, + cell: ({ row }) => + row.original.object_alias ? ( + + {row.original.object_alias} + + ) : ( + — + ), + }, { id: "changed_by", accessorKey: "changed_by", header: "Changed By", size: 200, enableSorting: false, - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { id: "changed_by_api_key", @@ -97,6 +162,8 @@ export const getAuditLogsTableColumns = ({ onViewLog }: AuditLogsTableColumnsDep header: "API Key (Hash)", size: 160, enableSorting: false, - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, ]; diff --git a/ui/litellm-dashboard/src/utils/entityLinks.ts b/ui/litellm-dashboard/src/utils/entityLinks.ts index 2659a307866..4dd6299deaa 100644 --- a/ui/litellm-dashboard/src/utils/entityLinks.ts +++ b/ui/litellm-dashboard/src/utils/entityLinks.ts @@ -3,3 +3,19 @@ import { migratedHref } from "@/utils/migratedPages"; export function teamDetailHref(teamId: string): string { return `${migratedHref("teams")}?team=${encodeURIComponent(teamId)}`; } + +export function keyDetailHref(keyTokenHash: string): string { + return `${migratedHref("api-keys")}?key=${encodeURIComponent(keyTokenHash)}`; +} + +export function userDetailHref(userId: string): string { + return `${migratedHref("users")}?user=${encodeURIComponent(userId)}`; +} + +export function orgDetailHref(organizationId: string): string { + return `${migratedHref("organizations")}?org=${encodeURIComponent(organizationId)}`; +} + +export function modelDetailHref(modelId: string): string { + return `${migratedHref("models-and-endpoints")}?model=${encodeURIComponent(modelId)}`; +}