diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx index 9dd4b1c2d59..6eea77ae827 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx @@ -9,17 +9,17 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/h import { Skeleton } from "@/components/ui/skeleton"; import { DateCell, + ENTITY_CELL_TITLE_CLASSES, IdCell, IdentityCell, ModelsCell, SpendBudgetCell, StatusBadge, + UserPopoverCell, type StatusTone, } from "@/components/shared/table_cells"; -import { orgDetailHref, teamDetailHref, userDetailHref } from "@/utils/entityLinks"; -import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels"; +import { orgDetailHref, teamDetailHref } from "@/utils/entityLinks"; -import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; import { KeyResponse, Team } from "../key_team_helpers/key_list"; import { Organization } from "../networking"; @@ -29,8 +29,6 @@ interface KeyStatus { tooltip?: string; } -const ENTITY_CELL_TITLE_CLASSES = "font-mono text-xs font-normal"; - const SPEND_BUDGET_SORT_FIELDS: DataTableSortField[] = [ { id: "spend", label: "Spend" }, { id: "max_budget", label: "Budget" }, @@ -66,60 +64,6 @@ const getKeyStatus = (key: KeyResponse): KeyStatus => { }; }; -const UserPopoverCell = ({ - userAlias, - userEmail, - userId, - width, -}: { - userAlias: string | null; - userEmail: string | null; - userId: string | null; - width: number; -}) => { - const displayValue = userAlias || userEmail || userId; - const isDefaultAdmin = userId === DEFAULT_PROXY_ADMIN_USER_ID; - - const popoverContent = ( -
- {[ - { label: "User Alias", value: userAlias }, - { label: "User Email", value: userEmail }, - { label: "User ID", value: userId }, - ].map(({ label, value }) => ( -
- {label} - {value ? ( - - ) : ( - - - )} -
- ))} -
- ); - - const trigger = - isDefaultAdmin && !userAlias && !userEmail ? ( - - ) : ( - - ); - - return ( - - }> - {trigger} - - {popoverContent} - - ); -}; - const InfoHeader = ({ label, tooltip }: { label: string; tooltip: string }) => ( {label} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/UserPopoverCell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/UserPopoverCell.tsx new file mode 100644 index 00000000000..223d66d7421 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/UserPopoverCell.tsx @@ -0,0 +1,62 @@ +"use client"; + +import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { userDetailHref } from "@/utils/entityLinks"; +import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels"; + +import { IdCell } from "./id_cell"; +import { IdentityCell } from "./identity_cell"; + +export const ENTITY_CELL_TITLE_CLASSES = "font-mono text-xs font-normal"; + +interface UserPopoverCellProps { + userAlias: string | null; + userEmail: string | null; + userId: string | null; + width: number; +} + +export function UserPopoverCell({ userAlias, userEmail, userId, width }: UserPopoverCellProps) { + const displayValue = userAlias || userEmail || userId; + const isDefaultAdmin = userId === DEFAULT_PROXY_ADMIN_USER_ID; + + const popoverContent = ( +
+ {[ + { label: "User Alias", value: userAlias }, + { label: "User Email", value: userEmail }, + { label: "User ID", value: userId }, + ].map(({ label, value }) => ( +
+ {label} + {value ? ( + + ) : ( + - + )} +
+ ))} +
+ ); + + const trigger = + isDefaultAdmin && !userAlias && !userEmail ? ( + + ) : ( + + ); + + return ( + + }> + {trigger} + + {popoverContent} + + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/index.ts b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts index 99b54f9aad0..34e3701eed4 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/index.ts +++ b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts @@ -13,3 +13,4 @@ export { ModelsCell } from "./models_cell"; export { MoneyCell } from "./money_cell"; export { SpendBudgetCell } from "./spend_budget_cell"; export { StatusBadge, type StatusTone } from "./status_badge"; +export { UserPopoverCell, ENTITY_CELL_TITLE_CLASSES } from "./UserPopoverCell"; diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx index 0d9d0988aa1..76df0d12539 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx @@ -10,6 +10,8 @@ vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ useKeys: vi.fn(), })); +vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); + vi.mock("../key_team_helpers/fetch_available_models_team_key", () => ({ getModelDisplayName: vi.fn((model: string) => model), })); @@ -384,4 +386,64 @@ describe("TeamVirtualKeysTable", () => { expect(screen.getByText("Key Info View")).toBeInTheDocument(); }); }); + + describe("entity links out of the key rows", () => { + const renderRow = async (key: KeyResponse, organization: Organization | null = null) => { + mockUseKeys.mockReturnValue({ + data: { keys: [key], total_count: 1, current_page: 1, total_pages: 1 } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + renderWithProviders(); + return (await screen.findByText(key.key_alias as string)).closest("tr") as HTMLElement; + }; + + it("points the Organization ID cell at the org's detail page", async () => { + const row = await renderRow(createMockKey({ organization_id: null }), mockOrganization); + expect(within(row).getByRole("link", { name: "org-123" })).toHaveAttribute( + "href", + "/ui/organizations?org=org-123", + ); + }); + + it("points the User Email and User ID cells at the owning user's detail page", async () => { + const row = await renderRow( + createMockKey({ user_id: "user-1", user: { user_id: "user-1", user_email: "alice@example.com" } }), + ); + expect(within(row).getByRole("link", { name: "alice@example.com" })).toHaveAttribute( + "href", + "/ui/users?user=user-1", + ); + expect(within(row).getByRole("link", { name: "user-1" })).toHaveAttribute("href", "/ui/users?user=user-1"); + }); + + it("points the Created By cell at the creator's detail page", async () => { + const row = await renderRow( + createMockKey({ + created_by: "creator-1", + created_by_user: { user_id: "creator-1", user_email: "creator@example.com", user_alias: "The Creator" }, + }), + ); + expect(within(row).getByRole("link", { name: "The Creator" })).toHaveAttribute( + "href", + "/ui/users?user=creator-1", + ); + }); + + it("leaves the default_user_id placeholder unlinked in the User ID and Created By cells", async () => { + const placeholder = { user_id: "default_user_id", user_email: "admin@example.com", user_alias: "Proxy Admin" }; + const row = await renderRow( + createMockKey({ + user_id: placeholder.user_id, + user: placeholder, + created_by: placeholder.user_id, + created_by_user: placeholder, + }), + ); + expect(within(row).getByText("Default Proxy Admin")).toBeInTheDocument(); + expect(within(row).getByText("Proxy Admin")).toBeInTheDocument(); + expect(within(row).queryByRole("link", { name: "Proxy Admin" })).not.toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index b0380255b95..5b1b71e060e 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -1,8 +1,14 @@ "use client"; import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; import { SimpleTooltip } from "@/components/ui/tooltip"; -import CopyButton from "@/components/shared/CopyButton"; -import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells"; +import { + DateCell, + ENTITY_CELL_TITLE_CLASSES, + IdCell, + IdentityCell, + MoneyCell, + UserPopoverCell, +} from "@/components/shared/table_cells"; import { DataTable, DataTableFilterDrawer, @@ -11,8 +17,9 @@ import { DataTableToolbar, } from "@/components/shared/DataTable"; import { Badge } from "@/components/ui/badge"; -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Input } from "@/components/ui/input"; +import { orgDetailHref, userDetailHref } from "@/utils/entityLinks"; +import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels"; import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants"; import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; import { ColumnDef, ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table"; @@ -168,7 +175,15 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi header: "Organization ID", size: 140, enableSorting: false, - cell: (info) => (info.getValue() ? info.renderValue() : "-"), + cell: (info) => { + const orgId = info.getValue() as string | null; + if (!orgId) return "-"; + return ( + + + + ); + }, }, { id: "user_email", @@ -179,9 +194,14 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi cell: (info) => { const user = info.getValue() as { user_email?: string } | undefined; const value = user?.user_email; + const userId = info.row.original.user_id; return ( - {value ?? "-"} + ); }, @@ -194,10 +214,16 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi enableSorting: false, cell: (info) => { const userId = info.getValue() as string | null; - const displayValue = userId === "default_user_id" ? "Default Proxy Admin" : userId; + if (userId === DEFAULT_PROXY_ADMIN_USER_ID) { + return ; + } return ( - - {displayValue ?? "-"} + + ); }, @@ -221,53 +247,13 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi const userId = info.getValue() as string | null; if (!userId) return "-"; const { created_by_user } = info.row.original; - const userAlias = created_by_user?.user_alias ?? null; - const userEmail = created_by_user?.user_email ?? null; - const isDefaultAdmin = userId === "default_user_id"; - const displayValue = userAlias || userEmail || userId; - - 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 ( - - }> - - - {popoverContent} - - ); - } - return ( - - } - > - {displayValue} - - {popoverContent} - + ); }, },