Merge pull request #40647 from BerriAI/litellm_team_keys_table_entity_links

feat(ui): link the entity cells on the team detail page's keys table
This commit is contained in:
ryan-crabbe-berri 2026-09-10 19:09:29 -07:00 committed by GitHub
commit dca71e214b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 181 additions and 114 deletions

View file

@ -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 = (
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
{[
{ label: "User Alias", value: userAlias },
{ label: "User Email", value: userEmail },
{ label: "User ID", value: userId },
].map(({ label, value }) => (
<div key={label} className="flex flex-col min-w-0">
<span className="text-muted-foreground">{label}</span>
{value ? (
<IdCell value={value} variant="plain" copyable className="max-w-full" />
) : (
<span className="font-mono">-</span>
)}
</div>
))}
</div>
);
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="block" style={{ maxWidth: width, overflow: "hidden" }} />}>
{trigger}
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
);
};
const InfoHeader = ({ label, tooltip }: { label: string; tooltip: string }) => (
<span className="flex items-center gap-1">
{label}

View file

@ -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 = (
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
{[
{ label: "User Alias", value: userAlias },
{ label: "User Email", value: userEmail },
{ label: "User ID", value: userId },
].map(({ label, value }) => (
<div key={label} className="flex flex-col min-w-0">
<span className="text-muted-foreground">{label}</span>
{value ? (
<IdCell value={value} variant="plain" copyable copyLabel={`Copy ${label}`} className="max-w-full" />
) : (
<span className="font-mono">-</span>
)}
</div>
))}
</div>
);
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="block" style={{ maxWidth: width, overflow: "hidden" }} />}>
{trigger}
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
);
}

View file

@ -71,6 +71,14 @@ describe("IdCell", () => {
expect(rowClick).not.toHaveBeenCalled();
});
it("names the copy button after the field it copies", async () => {
const user = userEvent.setup();
render(<IdCell value="alice@example.com" copyable copyLabel="Copy User Email" />);
expect(screen.queryByRole("button", { name: "Copy ID" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Copy User Email" }));
expect(copyToClipboardMock).toHaveBeenCalledWith("alice@example.com");
});
it("passes dataTestId through to the id element", () => {
render(<IdCell value="k-1" dataTestId="key-id-cell" />);
expect(screen.getByTestId("key-id-cell")).toHaveTextContent("k-1");

View file

@ -15,6 +15,7 @@ interface IdCellProps {
variant?: IdCellVariant;
onClick?: (value: string) => void;
copyable?: boolean;
copyLabel?: string;
truncate?: boolean;
fallback?: string;
tooltip?: React.ReactNode;
@ -39,6 +40,7 @@ export function IdCell({
variant = "pill",
onClick,
copyable = false,
copyLabel = "Copy ID",
truncate = true,
fallback = "-",
tooltip,
@ -80,7 +82,7 @@ export function IdCell({
{withTooltip}
<button
type="button"
aria-label="Copy ID"
aria-label={copyLabel}
className="shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
onClick={(event) => {
event.stopPropagation();

View file

@ -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";

View file

@ -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,66 @@ 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(<TeamVirtualKeysTable {...defaultProps} organization={organization} />);
await screen.findByText(key.key_alias as string);
return screen.getByRole("row", { name: new RegExp(key.key_alias as string) });
};
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 ownedAndCreatedByPlaceholder = {
user_id: placeholder.user_id,
user: placeholder,
created_by: placeholder.user_id,
created_by_user: placeholder,
};
const row = await renderRow(createMockKey(ownedAndCreatedByPlaceholder));
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();
expect(within(row).queryByRole("link", { name: placeholder.user_email })).not.toBeInTheDocument();
expect(within(row).queryByRole("link", { name: "Default Proxy Admin" })).not.toBeInTheDocument();
});
});
});

View file

@ -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 (
<SimpleTooltip content={orgId}>
<IdentityCell title={orgId} titleClassName={ENTITY_CELL_TITLE_CLASSES} href={orgDetailHref(orgId)} />
</SimpleTooltip>
);
},
},
{
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 (
<SimpleTooltip content={value}>
<span className="block max-w-full truncate font-mono text-xs">{value ?? "-"}</span>
<IdentityCell
title={value ?? "-"}
titleClassName={ENTITY_CELL_TITLE_CLASSES}
href={value && userId ? userDetailHref(userId) : undefined}
/>
</SimpleTooltip>
);
},
@ -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 <DefaultProxyAdminTag userId={userId} />;
}
return (
<SimpleTooltip content={displayValue}>
<span className="block max-w-full truncate font-mono text-xs">{displayValue ?? "-"}</span>
<SimpleTooltip content={userId}>
<IdentityCell
title={userId ?? "-"}
titleClassName={ENTITY_CELL_TITLE_CLASSES}
href={userId ? userDetailHref(userId) : undefined}
/>
</SimpleTooltip>
);
},
@ -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 = (
<div className="flex min-w-[200px] max-w-[300px] flex-col gap-2 text-xs">
{[
{ label: "User Alias", value: userAlias },
{ label: "User Email", value: userEmail },
{ label: "User ID", value: userId },
].map(({ label, value }) => (
<div key={label} className="flex flex-col min-w-0">
<span className="text-muted-foreground">{label}</span>
{value ? (
<span className="flex items-center gap-1">
<span className="min-w-0 flex-1 truncate font-mono text-xs">{value}</span>
<CopyButton value={value} label={`Copy ${label}`} />
</span>
) : (
<span className="font-mono">-</span>
)}
</div>
))}
</div>
);
if (isDefaultAdmin && !userAlias && !userEmail) {
return (
<HoverCard>
<HoverCardTrigger render={<span className="cursor-default" />}>
<DefaultProxyAdminTag userId={userId} />
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
);
}
return (
<HoverCard>
<HoverCardTrigger
render={<span className="block max-w-full cursor-default truncate font-mono text-xs" />}
>
{displayValue}
</HoverCardTrigger>
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
</HoverCard>
<UserPopoverCell
userAlias={created_by_user?.user_alias ?? null}
userEmail={created_by_user?.user_email ?? null}
userId={userId}
width={130}
/>
);
},
},