Merge pull request #40646 from BerriAI/litellm_key_table_entity_links

feat(ui): link the Team, Organization, User and Created By cells on the Virtual Keys page
This commit is contained in:
ryan-crabbe-berri 2026-09-10 18:27:12 -07:00 committed by GitHub
commit b1ba92ab0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 167 additions and 38 deletions

View file

@ -54,7 +54,7 @@ function ResourceBadge({
fallback,
}: {
resource: AccessGroupResource;
href: string;
href?: string;
fallback: (id: string) => string;
}) {
const badge = (

View file

@ -473,6 +473,92 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user
});
});
describe("entity links out of the key rows", () => {
const keyRow = async () => (await screen.findByText("Test Key Alias")).closest("tr") as HTMLElement;
const enableColumn = async (user: ReturnType<typeof userEvent.setup>, title: string) => {
await user.click(screen.getByRole("button", { name: "Columns" }));
await user.click(await screen.findByText(title));
await user.keyboard("{Escape}");
};
const enableCreatedByColumn = (user: ReturnType<typeof userEvent.setup>) => enableColumn(user, "Created By");
it("points the User and Team cells at their detail pages", async () => {
renderWithProviders(<VirtualKeysTable />);
const row = await keyRow();
expect(within(row).getByRole("link", { name: "user@example.com" })).toHaveAttribute(
"href",
"/ui/users?user=user-1",
);
expect(within(row).getByRole("link", { name: "Test Team" })).toHaveAttribute("href", "/ui/teams?team=team-1");
});
it("points the Organization cell at the org's detail page", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, org_id: "org-1" }]));
const user = userEvent.setup();
renderWithProviders(<VirtualKeysTable />);
await enableColumn(user, "Organization");
const row = await keyRow();
expect(within(row).getByRole("link", { name: "Test Organization" })).toHaveAttribute(
"href",
"/ui/organizations?org=org-1",
);
});
it("points the Created By cell at the creator's detail page", async () => {
mockUseKeys.mockReturnValue(
keysResult([
{
...mockKey,
created_by: "creator-1",
created_by_user: { user_id: "creator-1", user_email: "creator@example.com", user_alias: "The Creator" },
},
]),
);
const user = userEvent.setup();
renderWithProviders(<VirtualKeysTable />);
await enableCreatedByColumn(user);
const row = await keyRow();
expect(within(row).getByRole("link", { name: "The Creator" })).toHaveAttribute("href", "/ui/users?user=creator-1");
});
it("leaves the default_user_id placeholder unlinked even once it resolves to a named user", async () => {
const placeholder = { user_id: "default_user_id", user_email: "admin@example.com", user_alias: "Proxy Admin" };
mockUseKeys.mockReturnValue(
keysResult([
{
...mockKey,
user_id: placeholder.user_id,
user_email: placeholder.user_email,
user: placeholder,
created_by: placeholder.user_id,
created_by_user: placeholder,
},
]),
);
const user = userEvent.setup();
renderWithProviders(<VirtualKeysTable />);
await enableCreatedByColumn(user);
const row = await keyRow();
expect(within(row).getAllByText("Proxy Admin")).toHaveLength(2);
expect(within(row).queryByRole("link", { name: "Proxy Admin" })).not.toBeInTheDocument();
});
it("leaves the litellm-dashboard session team unlinked", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, team_id: "litellm-dashboard" }]));
renderWithProviders(<VirtualKeysTable />);
const row = await keyRow();
expect(within(row).getByText("litellm-dashboard")).toBeInTheDocument();
expect(within(row).queryByRole("link", { name: "litellm-dashboard" })).not.toBeInTheDocument();
});
});
it("should render table without crashing when models is null", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, models: null as unknown as string[] }]));

View file

@ -16,6 +16,8 @@ import {
StatusBadge,
type StatusTone,
} from "@/components/shared/table_cells";
import { orgDetailHref, teamDetailHref, userDetailHref } from "@/utils/entityLinks";
import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
@ -27,6 +29,8 @@ 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" },
@ -74,7 +78,7 @@ const UserPopoverCell = ({
width: number;
}) => {
const displayValue = userAlias || userEmail || userId;
const isDefaultAdmin = userId === "default_user_id";
const isDefaultAdmin = userId === DEFAULT_PROXY_ADMIN_USER_ID;
const popoverContent = (
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
@ -95,28 +99,21 @@ const UserPopoverCell = ({
</div>
);
if (isDefaultAdmin && !userAlias && !userEmail) {
return (
<HoverCard>
<HoverCardTrigger render={<span className="cursor-default" />}>
<DefaultProxyAdminTag userId={userId} />
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
const trigger =
isDefaultAdmin && !userAlias && !userEmail ? (
<DefaultProxyAdminTag userId={userId} />
) : (
<IdentityCell
title={displayValue || "-"}
titleClassName={ENTITY_CELL_TITLE_CLASSES}
href={userId ? userDetailHref(userId) : undefined}
/>
);
}
return (
<HoverCard>
<HoverCardTrigger
render={
<span
className="font-mono text-xs truncate block cursor-default"
style={{ maxWidth: width, overflow: "hidden" }}
/>
}
>
{displayValue || "-"}
<HoverCardTrigger render={<span className="block" style={{ maxWidth: width, overflow: "hidden" }} />}>
{trigger}
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
@ -201,12 +198,12 @@ export const getKeyTableColumns = ({
const teamId = info.getValue() as string | null;
if (!teamId) return "-";
const team = allTeams.find((t) => t.team_id === teamId);
const displayValue = team?.team_alias || teamId;
const width = info.cell.column.getSize();
return (
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
{displayValue}
</span>
<IdentityCell
title={team?.team_alias || teamId}
titleClassName={ENTITY_CELL_TITLE_CLASSES}
href={teamDetailHref(teamId)}
/>
);
},
},
@ -221,12 +218,12 @@ export const getKeyTableColumns = ({
const orgId = info.getValue() as string | null;
if (!orgId) return "-";
const org = organizations.find((o) => o.organization_id === orgId);
const displayValue = org?.organization_alias || orgId;
const width = info.cell.column.getSize();
return (
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
{displayValue}
</span>
<IdentityCell
title={org?.organization_alias || orgId}
titleClassName={ENTITY_CELL_TITLE_CLASSES}
href={orgDetailHref(orgId)}
/>
);
},
},

View file

@ -1,13 +1,12 @@
import { Badge } from "@/components/ui/badge";
const DEFAULT_USER_ID = "default_user_id";
import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels";
interface DefaultProxyAdminTagProps {
userId: string | null | undefined;
}
export default function DefaultProxyAdminTag({ userId }: DefaultProxyAdminTagProps) {
if (userId === DEFAULT_USER_ID) {
if (userId === DEFAULT_PROXY_ADMIN_USER_ID) {
return <Badge variant="secondary">Default Proxy Admin</Badge>;
}

View file

@ -2,6 +2,7 @@ import React from "react";
import CopyButton from "@/components/shared/CopyButton";
import { EntityLink } from "@/components/shared/EntityLink";
import { cx } from "@/lib/cva.config";
import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels";
import DefaultProxyAdminTag from "./DefaultProxyAdminTag";
interface LabeledFieldProps {
@ -24,7 +25,7 @@ export default function LabeledField({
defaultUserIdCheck = false,
}: LabeledFieldProps) {
const isEmpty = !value;
const isDefaultUser = defaultUserIdCheck && value === "default_user_id";
const isDefaultUser = defaultUserIdCheck && value === DEFAULT_PROXY_ADMIN_USER_ID;
const displayValue = isEmpty ? "-" : value;
const isCopyable = copyable && !isEmpty && !isDefaultUser;
const isLink = href != null && !isEmpty && !isDefaultUser;

View file

@ -25,6 +25,12 @@ describe("EntityLink", () => {
expect(push).toHaveBeenCalledWith("/ui/users?user=u1");
});
it("renders the label as plain text when there is no href to point at", () => {
render(<EntityLink>default_user_id</EntityLink>);
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.getByText("default_user_id")).toBeInTheDocument();
});
it("leaves modified clicks to the browser so new-tab shortcuts keep working", async () => {
const user = userEvent.setup();
render(<EntityLink href="/ui/users?user=u1">alice</EntityLink>);

View file

@ -19,12 +19,24 @@ export function useEntityLinkClick(href: string): (e: React.MouseEvent) => void
}
interface EntityLinkProps {
href: string;
href?: string;
className?: string;
children: React.ReactNode;
}
export function EntityLink({ href, className, children }: EntityLinkProps) {
if (!href) {
return <span className={cn("inline-block min-w-0 max-w-full truncate font-semibold", className)}>{children}</span>;
}
return (
<LinkedEntity href={href} className={className}>
{children}
</LinkedEntity>
);
}
function LinkedEntity({ href, className, children }: EntityLinkProps & { href: string }) {
const handleClick = useEntityLinkClick(href);
return (

View file

@ -2,7 +2,29 @@ import { describe, expect, it, vi } from "vitest";
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { modelGroupHref } from "./entityLinks";
import { modelGroupHref, teamDetailHref, userDetailHref } from "./entityLinks";
describe("userDetailHref", () => {
it("targets the users page filtered to the encoded user id", () => {
expect(userDetailHref("user-1")).toMatch(/\/users\?user=user-1$/);
expect(userDetailHref("a b/c")).toMatch(/\?user=a%20b%2Fc$/);
});
it("returns no href for the proxy admin placeholder, which has no user page", () => {
expect(userDetailHref("default_user_id")).toBeUndefined();
});
});
describe("teamDetailHref", () => {
it("targets the teams page filtered to the encoded team id", () => {
expect(teamDetailHref("team-1")).toMatch(/\/teams\?team=team-1$/);
expect(teamDetailHref("a b/c")).toMatch(/\?team=a%20b%2Fc$/);
});
it("returns no href for the Admin UI session team, which has no team page", () => {
expect(teamDetailHref("litellm-dashboard")).toBeUndefined();
});
});
describe("modelGroupHref", () => {
it("targets the models page filtered to the encoded model group", () => {

View file

@ -1,3 +1,4 @@
import { DEFAULT_PROXY_ADMIN_USER_ID, UI_TEAM_ID } from "@/utils/sentinels";
import { uiHref } from "@/utils/uiHref";
const MODEL_GRANT_SENTINELS: ReadonlySet<string> = new Set([
@ -6,7 +7,8 @@ const MODEL_GRANT_SENTINELS: ReadonlySet<string> = new Set([
"no-default-models",
]);
export function teamDetailHref(teamId: string): string {
export function teamDetailHref(teamId: string): string | undefined {
if (teamId === UI_TEAM_ID) return undefined;
return `${uiHref("teams")}?team=${encodeURIComponent(teamId)}`;
}
@ -14,7 +16,8 @@ export function keyDetailHref(keyToken: string): string {
return `${uiHref("api-keys")}?key=${encodeURIComponent(keyToken)}`;
}
export function userDetailHref(userId: string): string {
export function userDetailHref(userId: string): string | undefined {
if (userId === DEFAULT_PROXY_ADMIN_USER_ID) return undefined;
return `${uiHref("users")}?user=${encodeURIComponent(userId)}`;
}

View file

@ -0,0 +1,3 @@
export const DEFAULT_PROXY_ADMIN_USER_ID = "default_user_id";
export const UI_TEAM_ID = "litellm-dashboard";