From 9d83a6f814454a6b2f8374c16c571c31d58252c7 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 10 Mar 2026 16:55:48 -0700 Subject: [PATCH] [Feature] UI - Keys: Improve VirtualKeysTable column display and usability Consolidate User Email and User ID columns into a single "User" column with fallback display (Alias > Email > ID) and hover popover with copyable values. Resolve Team and Organization columns to show aliases instead of raw UUIDs. Co-Authored-By: Claude Opus 4.6 --- .../VirtualKeysPage/VirtualKeysTable.test.tsx | 17 +- .../VirtualKeysPage/VirtualKeysTable.tsx | 197 ++++++++++++------ .../components/key_team_helpers/key_list.tsx | 1 + 3 files changed, 147 insertions(+), 68 deletions(-) diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index 35f87e8770e..418bca64ea9 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -57,6 +57,18 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({ default: vi.fn(), })); +// Mock useOrganizations hook +vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ + useOrganizations: vi.fn().mockReturnValue({ + data: [ + { + organization_id: "org-1", + organization_alias: "Test Organization", + }, + ], + }), +})); + // Mock fetchTeams to prevent network calls vi.mock("@/app/(dashboard)/networking", async (importOriginal) => { const actual = await importOriginal(); @@ -125,6 +137,7 @@ const mockKey: KeyResponse = { user: { user_email: "user@example.com", user_id: "user-1", + user_alias: null, }, }; @@ -380,7 +393,7 @@ it("should render table headers correctly", () => { // Check that main headers are rendered (testing the header.isPlaceholder condition path) expect(screen.getByText("Key ID")).toBeInTheDocument(); expect(screen.getByText("Key Alias")).toBeInTheDocument(); - expect(screen.getByText("Team Alias")).toBeInTheDocument(); + expect(screen.getByText("Team")).toBeInTheDocument(); expect(screen.getByText("Models")).toBeInTheDocument(); expect(screen.getByText("Spend (USD)")).toBeInTheDocument(); }); @@ -463,6 +476,8 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user const keyWithDefaultUserId = { ...mockKey, user_id: "default_user_id", + user_email: "", + user: { user_id: "default_user_id", user_email: "", user_alias: null }, }; mockUseFilterLogic.mockReturnValue({ diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx index d9c590938fb..20cc1b8153c 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx @@ -1,5 +1,6 @@ "use client"; import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; import { @@ -25,13 +26,14 @@ import { Text, } from "@tremor/react"; import { InfoCircleOutlined, SyncOutlined } from "@ant-design/icons"; -import { Button as AntButton, Popover, Skeleton, Tooltip } from "antd"; +import { Button as AntButton, Popover, Skeleton, Tooltip, Typography } from "antd"; import React, { useEffect, useDeferredValue, useMemo, useState } from "react"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; import { useFilterLogic } from "../key_team_helpers/filter_logic"; import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect"; import { KeyResponse, Team } from "../key_team_helpers/key_list"; import FilterComponent, { FilterOption } from "../molecules/filter"; +import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; import { Organization } from "../networking"; import KeyInfoView from "../templates/key_info_view"; @@ -51,6 +53,8 @@ interface VirtualKeysTableProps { */ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSort }: VirtualKeysTableProps) { + const { data: fetchedOrganizations } = useOrganizations(); + const resolvedOrganizations = fetchedOrganizations ?? organizations ?? []; const [selectedKey, setSelectedKey] = useState(null); const [sorting, setSorting] = React.useState(() => { if (currentSort) { @@ -86,6 +90,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo } = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize, { sortBy: sortBy || undefined, sortOrder: sortOrder || undefined, + expand: "user", }); const [expandedAccordions, setExpandedAccordions] = useState>({}); @@ -172,11 +177,9 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo const value = info.getValue() as string; const width = info.cell.column.getSize(); return ( - - - {value ?? "-"} - - + + {value ?? "-"} + ); }, }, @@ -191,76 +194,110 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo { id: "team_alias", accessorKey: "team_id", - header: "Team Alias", + header: "Team", size: 120, enableSorting: false, - cell: ({ row, getValue }) => { - const teamId = getValue() as string; - const team = teams?.find((t) => t.team_id === teamId); - return team?.team_alias || "Unknown"; - }, - }, - { - id: "team_id", - accessorKey: "team_id", - header: "Team ID", - size: 80, - enableSorting: false, cell: (info) => { - const value = info.getValue() as string | null; + const teamId = info.getValue() as string | null; + if (!teamId) return "-"; + const team = teams?.find((t) => t.team_id === teamId); + const displayValue = team?.team_alias || teamId; const width = info.cell.column.getSize(); return ( - - - {value ?? "-"} - - + + {displayValue} + ); }, }, { - id: "organization_id", + id: "organization_alias", accessorKey: "org_id", - header: "Organization ID", + header: "Organization", size: 140, enableSorting: false, - cell: (info) => (info.getValue() ? info.renderValue() : "-"), - }, - { - id: "user_email", - accessorKey: "user", - header: "User Email", - size: 160, - enableSorting: false, cell: (info) => { - const user = info.getValue() as any; - const value = user?.user_email; + const orgId = info.getValue() as string | null; + if (!orgId) return "-"; + const org = resolvedOrganizations.find((o) => o.organization_id === orgId); + const displayValue = org?.organization_alias || orgId; const width = info.cell.column.getSize(); return ( - - - {value ?? "-"} - - + + {displayValue} + ); }, }, { - id: "user_id", - accessorKey: "user_id", - header: "User ID", - size: 70, + id: "user", + accessorKey: "user", + header: () => ( + + User + + + + + ), + size: 160, enableSorting: false, - cell: (info) => { - const userId = info.getValue() as string | null; - const displayValue = userId === "default_user_id" ? "Default Proxy Admin" : userId; - const width = info.cell.column.getSize(); + cell: ({ row }) => { + const key = row.original; + const userAlias = key.user?.user_alias ?? null; + const userEmail = key.user?.user_email ?? key.user_email ?? null; + const userId = key.user_id ?? null; + const isDefaultAdmin = userId === "default_user_id"; + const displayValue = userAlias || userEmail || userId; + const width = 160; + + const popoverContent = ( +
+ {[ + { label: "User Alias", value: userAlias }, + { label: "User Email", value: userEmail }, + { label: "User ID", value: userId }, + ].map(({ label, value }) => ( +
+ {label} + {value ? ( + + {value} + + ) : ( + - + )} +
+ ))} +
+ ); + + if (isDefaultAdmin && !userAlias && !userEmail) { + return ( + + + + + + ); + } + return ( - - - {displayValue ?? "-"} + + + {displayValue || "-"} - + ); }, }, @@ -279,18 +316,48 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo id: "created_by", accessorKey: "created_by", header: "Created By", - size: 70, + size: 160, enableSorting: false, cell: (info) => { - const value = info.getValue() as string | null; - const displayValue = value === "default_user_id" ? "Default Proxy Admin" : value; - const width = info.cell.column.getSize(); + const userId = info.getValue() as string | null; + if (!userId) return "-"; + const isDefaultAdmin = userId === "default_user_id"; + const width = 160; + + const popoverContent = ( +
+
+ User ID + + {userId} + +
+
+ ); + + if (isDefaultAdmin) { + return ( + + + + + + ); + } + return ( - - - {displayValue ?? "-"} + + + {userId} - + ); }, }, @@ -477,7 +544,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo ); }, }, - ], []); + ], [teams, resolvedOrganizations]); const filterOptions: FilterOption[] = [ { @@ -535,8 +602,6 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo }, ]; - console.log(`keys: ${JSON.stringify(keys)}`); - const table = useReactTable({ data: filteredKeys, columns: columns.filter((col) => col.id !== "expander"), @@ -548,13 +613,11 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo }, onSortingChange: (updaterOrValue) => { const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; - console.log(`newSorting: ${JSON.stringify(newSorting)}`); setSorting(newSorting); if (newSorting && newSorting.length > 0) { const sortState = newSorting[0]; const sortBy = sortState.id; const sortOrder = sortState.desc ? "desc" : "asc"; - console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`); // Update filters state without triggering debouncedSearch // The useKeys hook will automatically refetch with the new sort parameters handleFilterChange( diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 4cc3367f71d..a31162cb2f2 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -99,6 +99,7 @@ export interface KeyResponse { user?: { user_id: string; user_email: string; + user_alias: string | null; }; }