feat(ui): link the entity cells on the team detail page's keys table

The team detail page's Virtual Keys table showed Organization ID, User
Email, User ID and Created By as dead text, so getting from a key to the
org or user behind it meant copying an id and searching for it.

Those four cells now render as links, reusing the sentinel-aware href
helpers, so default_user_id and the litellm-dashboard team stay plain
text instead of pointing at pages that do not exist.

The Created By cell was a verbatim copy of the Virtual Keys page's user
popover, so that moved into the shared table_cells kit and both tables
now use the one implementation.

Claude-Session: https://claude.ai/code/session_01NfwfQhamRNnSqgXMUjf3h4
This commit is contained in:
ryan-crabbe-berri 2026-09-10 18:08:13 -07:00
parent 880ccc76a5
commit 56e2d8846d
5 changed files with 168 additions and 113 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 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

@ -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,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(<TeamVirtualKeysTable {...defaultProps} organization={organization} />);
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();
});
});
});

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}
/>
);
},
},