Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/patch-endpoint-key-update-65648c

This commit is contained in:
Yuneng Jiang 2026-08-17 10:33:51 -07:00
commit 8512bbc2c5
No known key found for this signature in database
14 changed files with 374 additions and 26 deletions

View file

@ -23,6 +23,8 @@ vi.mock("@tanstack/react-pacer/debouncer", async () => {
};
});
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(() => ({
accessToken: "test-token",

View file

@ -1,7 +1,9 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import LabeledField from "./LabeledField";
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
describe("LabeledField", () => {
it("should render the label and value", () => {
render(<LabeledField label="User Email" value="test@example.com" />);
@ -50,4 +52,34 @@ describe("LabeledField", () => {
render(<LabeledField label="User ID" value="user-123" copyable />);
expect(screen.getByRole("button", { name: "Copy User ID" })).toBeInTheDocument();
});
it("should render the value as a link when href is provided", () => {
render(<LabeledField label="Team" value="my-team" href="/ui/teams?team=t1" />);
expect(screen.getByRole("link", { name: "my-team" })).toHaveAttribute("href", "/ui/teams?team=t1");
});
it("should keep the copy button next to a linked value", () => {
render(<LabeledField label="Created By" value="alice" href="/ui/users?user=u1" copyable />);
expect(screen.getByRole("link", { name: "alice" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Copy Created By" })).toBeInTheDocument();
});
it("should not link an empty value even when href is provided", () => {
render(<LabeledField label="Team" value="" href="/ui/teams?team=t1" />);
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
});
it("should not link the Default Proxy Admin tag", () => {
render(
<LabeledField
label="Created By"
value="default_user_id"
href="/ui/users?user=default_user_id"
defaultUserIdCheck
/>,
);
expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
expect(screen.queryByRole("link")).not.toBeInTheDocument();
});
});

View file

@ -1,5 +1,6 @@
import React from "react";
import CopyButton from "@/components/shared/CopyButton";
import { EntityLink } from "@/components/shared/EntityLink";
import { cx } from "@/lib/cva.config";
import DefaultProxyAdminTag from "./DefaultProxyAdminTag";
@ -7,6 +8,7 @@ interface LabeledFieldProps {
label: string;
value: string;
icon?: React.ReactNode;
href?: string;
truncate?: boolean;
copyable?: boolean;
defaultUserIdCheck?: boolean;
@ -16,6 +18,7 @@ export default function LabeledField({
label,
value,
icon,
href,
truncate = false,
copyable = false,
defaultUserIdCheck = false,
@ -24,14 +27,21 @@ export default function LabeledField({
const isDefaultUser = defaultUserIdCheck && value === "default_user_id";
const displayValue = isEmpty ? "-" : value;
const isCopyable = copyable && !isEmpty && !isDefaultUser;
const isLink = href != null && !isEmpty && !isDefaultUser;
const valueEl = isDefaultUser ? (
<DefaultProxyAdminTag userId={value} />
) : (
<span className="inline-flex min-w-0 items-center gap-1">
<strong className={cx("font-semibold", truncate ? "block max-w-40 truncate" : "break-words")}>
{displayValue}
</strong>
{isLink ? (
<EntityLink href={href} className={cx(truncate && "max-w-40")}>
{displayValue}
</EntityLink>
) : (
<strong className={cx("font-semibold", truncate ? "block max-w-40 truncate" : "break-words")}>
{displayValue}
</strong>
)}
{isCopyable && <CopyButton value={value} label={`Copy ${label}`} />}
</span>
);

View file

@ -1,8 +1,8 @@
"use client";
import { useRouter } from "next/navigation";
import * as React from "react";
import { useEntityLinkClick } from "@/components/shared/EntityLink";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/cva.config";
@ -16,8 +16,6 @@ interface BadgeLinkProps {
}
export function BadgeLink({ href, variant = "secondary", className, children }: BadgeLinkProps) {
const router = useRouter();
if (!href) {
return (
<Badge variant={variant} className={cn(ENTITY_BADGE_SIZE, className)}>
@ -26,13 +24,15 @@ export function BadgeLink({ href, variant = "secondary", className, children }:
);
}
const handleClick = (e: React.MouseEvent) => {
const hasModifierKey = e.metaKey || e.ctrlKey || e.shiftKey;
const isNativeNewTabClick = hasModifierKey || e.button === 1;
if (isNativeNewTabClick) return;
e.preventDefault();
router.push(href);
};
return (
<LinkedBadge href={href} variant={variant} className={className}>
{children}
</LinkedBadge>
);
}
function LinkedBadge({ href, variant, className, children }: BadgeLinkProps & { href: string }) {
const handleClick = useEntityLinkClick(href);
return (
<Badge

View file

@ -0,0 +1,36 @@
/* @vitest-environment jsdom */
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EntityLink } from "./EntityLink";
const push = vi.fn();
vi.mock("next/navigation", () => ({ useRouter: () => ({ push }) }));
describe("EntityLink", () => {
beforeEach(() => {
push.mockClear();
});
it("renders an anchor pointing at the target href", () => {
render(<EntityLink href="/ui/users?user=u1">alice</EntityLink>);
expect(screen.getByRole("link", { name: "alice" })).toHaveAttribute("href", "/ui/users?user=u1");
});
it("navigates client-side on plain click", async () => {
const user = userEvent.setup();
render(<EntityLink href="/ui/users?user=u1">alice</EntityLink>);
await user.click(screen.getByRole("link", { name: "alice" }));
expect(push).toHaveBeenCalledWith("/ui/users?user=u1");
});
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>);
await user.keyboard("{Meta>}");
await user.click(screen.getByRole("link", { name: "alice" }));
await user.keyboard("{/Meta}");
expect(push).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,43 @@
"use client";
import { ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import * as React from "react";
import { cn } from "@/lib/cva.config";
export function useEntityLinkClick(href: string): (e: React.MouseEvent) => void {
const router = useRouter();
return (e: React.MouseEvent) => {
const hasModifierKey = e.metaKey || e.ctrlKey || e.shiftKey;
const isNativeNewTabClick = hasModifierKey || e.button === 1;
if (isNativeNewTabClick) return;
e.preventDefault();
router.push(href);
};
}
interface EntityLinkProps {
href: string;
className?: string;
children: React.ReactNode;
}
export function EntityLink({ href, className, children }: EntityLinkProps) {
const handleClick = useEntityLinkClick(href);
return (
<a
href={href}
onClick={handleClick}
className={cn(
"group inline-flex min-w-0 max-w-full items-center gap-0.5 font-semibold underline-offset-4 hover:underline",
className,
)}
>
<span className="min-w-0 truncate">{children}</span>
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
</a>
);
}

View file

@ -1,9 +1,9 @@
"use client";
import { ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import * as React from "react";
import { useEntityLinkClick } from "@/components/shared/EntityLink";
import { cn } from "@/lib/cva.config";
interface IdentityCellProps {
@ -57,15 +57,7 @@ export function IdentityCell({ title, subtitle, badge, onClick, href, className,
}
function IdentityCellLink({ href, className, body }: { href: string; className?: string; body: React.ReactNode }) {
const router = useRouter();
const handleClick = (e: React.MouseEvent) => {
const hasModifierKey = e.metaKey || e.ctrlKey || e.shiftKey;
const isNativeNewTabClick = hasModifierKey || e.button === 1;
if (isNativeNewTabClick) return;
e.preventDefault();
router.push(href);
};
const handleClick = useEntityLinkClick(href);
return (
<a href={href} onClick={handleClick} className={cn(INTERACTIVE_CELL_CLASSES, className)}>

View file

@ -3,13 +3,20 @@ import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { KeyInfoHeader, KeyInfoData } from "./KeyInfoHeader";
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
const MOCK_DATA: KeyInfoData = {
keyName: "My Test Key",
keyId: "sk-1234567890abcdef",
userId: "user-abc-123",
userEmail: "test@example.com",
userAlias: null,
teamId: "team-xyz-789",
teamAlias: "Platform Team",
orgId: "org-abc-001",
orgAlias: "Acme Org",
createdBy: "admin@example.com",
createdById: "admin-user-456",
createdAt: "Oct 29, 2025 at 1:26 AM",
lastUpdated: "Oct 29, 2025 at 1:47 AM",
lastActive: "Oct 29, 2025 at 2:00 AM",
@ -37,6 +44,74 @@ describe("KeyInfoHeader", () => {
expect(screen.getByText("Expires")).toBeInTheDocument();
expect(screen.getByText("Last Updated")).toBeInTheDocument();
expect(screen.getByText("Last Active")).toBeInTheDocument();
expect(screen.getByText("Team")).toBeInTheDocument();
expect(screen.getByText("Organization")).toBeInTheDocument();
});
describe("entity links", () => {
it("links the user to the users page", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("link", { name: "test@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=user-abc-123"),
);
});
it("links the creator to the users page by user id, not by the displayed alias", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("link", { name: "admin@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=admin-user-456"),
);
});
it("shows the team alias and links it to the team page by id", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("link", { name: "Platform Team" })).toHaveAttribute(
"href",
expect.stringContaining("/teams?team=team-xyz-789"),
);
});
it("falls back to the team id when no alias is known", () => {
render(<KeyInfoHeader data={{ ...MOCK_DATA, teamAlias: null }} />);
expect(screen.getByRole("link", { name: "team-xyz-789" })).toHaveAttribute(
"href",
expect.stringContaining("/teams?team=team-xyz-789"),
);
});
it("shows the organization alias and links it to the organization page by id", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("link", { name: "Acme Org" })).toHaveAttribute(
"href",
expect.stringContaining("/organizations?org=org-abc-001"),
);
});
it("renders '-' without a link when the key has no organization", () => {
render(<KeyInfoHeader data={{ ...MOCK_DATA, orgId: "", orgAlias: null }} />);
expect(screen.queryByRole("link", { name: /org/i })).not.toBeInTheDocument();
expect(screen.getByText("Organization").parentElement?.parentElement).toHaveTextContent("-");
});
it("renders '-' without a link when the key has no team", () => {
render(<KeyInfoHeader data={{ ...MOCK_DATA, teamId: "", teamAlias: null }} />);
expect(screen.queryByRole("link", { name: /team/i })).not.toBeInTheDocument();
expect(screen.getByText("Team").parentElement?.parentElement).toHaveTextContent("-");
});
it("does not link the user when the key has no user id", () => {
render(<KeyInfoHeader data={{ ...MOCK_DATA, userId: "", userEmail: "orphan@example.com" }} />);
expect(screen.getByText("orphan@example.com")).toBeInTheDocument();
expect(screen.queryByRole("link", { name: "orphan@example.com" })).not.toBeInTheDocument();
});
it("keeps the Default Proxy Admin creator unlinked", () => {
render(<KeyInfoHeader data={{ ...MOCK_DATA, createdBy: "default_user_id", createdById: "default_user_id" }} />);
expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
expect(screen.queryByRole("link", { name: /default/i })).not.toBeInTheDocument();
});
});
describe("back button", () => {

View file

@ -3,6 +3,7 @@ import {
ArrowLeft,
ArrowLeftRight,
Ban,
Building2,
Calendar,
CircleCheck,
Clock,
@ -13,6 +14,7 @@ import {
Timer,
Trash2,
User,
Users,
Zap,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
@ -27,6 +29,8 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/h
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import CopyButton from "@/components/shared/CopyButton";
import { EntityLink } from "@/components/shared/EntityLink";
import { orgDetailHref, teamDetailHref, userDetailHref } from "@/utils/entityLinks";
import LabeledField from "../common_components/LabeledField";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
@ -36,7 +40,12 @@ export interface KeyInfoData {
userId: string;
userEmail: string;
userAlias?: string | null;
teamId: string;
teamAlias?: string | null;
orgId: string;
orgAlias?: string | null;
createdBy: string;
createdById: string;
createdAt: string;
lastUpdated: string;
lastActive: string;
@ -135,7 +144,11 @@ function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null
<div>
<HoverCard>
<HoverCardTrigger
render={<span className="block max-w-[200px] cursor-default truncate font-semibold">{displayValue}</span>}
render={
<span className="block max-w-[200px] cursor-default truncate font-semibold">
{userId ? <EntityLink href={userDetailHref(userId)}>{displayValue}</EntityLink> : displayValue}
</span>
}
/>
<HoverCardContent side="bottom" align="start" className="w-auto">
{popoverContent}
@ -265,6 +278,7 @@ export function KeyInfoHeader({
label="Created By"
value={data.createdBy}
icon={<ShieldCheck className="size-3.5" />}
href={data.createdById ? userDetailHref(data.createdById) : undefined}
truncate
copyable
defaultUserIdCheck
@ -277,6 +291,25 @@ export function KeyInfoHeader({
<LabeledField label="Last Updated" value={data.lastUpdated} icon={<Clock className="size-3.5" />} />
<LabeledField label="Last Active" value={data.lastActive} icon={<Zap className="size-3.5" />} />
</div>
<Separator orientation="vertical" />
<div className="flex min-w-0 flex-col gap-4">
<LabeledField
label="Team"
value={data.teamAlias || data.teamId}
icon={<Users className="size-3.5" />}
href={data.teamId ? teamDetailHref(data.teamId) : undefined}
truncate
/>
<LabeledField
label="Organization"
value={data.orgAlias || data.orgId}
icon={<Building2 className="size-3.5" />}
href={data.orgId ? orgDetailHref(data.orgId) : undefined}
truncate
/>
</div>
</div>
</div>
);

View file

@ -17,9 +17,14 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: mockUseAuthorized,
}));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: () => ({ data: [] }),
}));
// Networking: wire the hoisted fns so we can assert calls later
vi.mock("../networking", () => {
return {
serverRootPath: "",
keyUpdateCall: (...args: any[]) => keyUpdateCallMock(...args),
keyDeleteCall: (...args: any[]) => keyDeleteCallMock(...args),
};

View file

@ -11,6 +11,12 @@ import useTeams from "@/app/(dashboard)/hooks/useTeams";
// where the overview "Spend" card formatted `max_budget` with the default 0
// decimals, truncating sub-dollar budgets (e.g. $0.10) to "$0".
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: () => ({ data: [] }),
}));
vi.mock("./key_edit_view", () => ({
KeyEditView: () => <div data-testid="key-edit-view-stub" />,
}));
@ -24,6 +30,7 @@ vi.mock("@/app/(dashboard)/hooks/keys/useResetKeySpend", () => ({
useResetKeySpend: vi.fn(() => ({ mutate: vi.fn(), isPending: false })),
}));
vi.mock("../networking", () => ({
serverRootPath: "",
keyDeleteCall: vi.fn().mockResolvedValue({}),
keyUpdateCall: vi.fn().mockResolvedValue({}),
getPolicyInfoWithGuardrails: vi.fn().mockResolvedValue({ resolved_guardrails: [] }),

View file

@ -13,6 +13,12 @@ const editViewMocks = vi.hoisted(() => ({
onSubmit: undefined as ((v: Record<string, any>) => Promise<void>) | undefined,
}));
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: () => ({ data: [] }),
}));
vi.mock("./key_edit_view", () => ({
KeyEditView: ({ onSubmit }: { onSubmit: (v: Record<string, any>) => Promise<void> }) => {
editViewMocks.onSubmit = onSubmit;
@ -53,6 +59,7 @@ import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"
import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolsets";
vi.mock("../networking", () => ({
serverRootPath: "",
keyDeleteCall: vi.fn().mockResolvedValue({}),
keyUpdateCall: vi.fn().mockResolvedValue({}),
getPolicyInfoWithGuardrails: vi.fn().mockResolvedValue({
@ -487,6 +494,89 @@ describe("KeyInfoView", () => {
});
});
describe("entity links in the header", () => {
const mockTeam: Team = {
team_id: "linked-team-id",
team_alias: "Linked Team",
models: [],
max_budget: null,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2025-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
spend: 0,
};
beforeEach(() => {
vi.mocked(useTeams).mockReturnValue({ teams: [mockTeam], setTeams: vi.fn() });
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
});
it("links the key's team by alias, resolved from the teams list, to the team page", async () => {
const keyData = { ...MOCK_KEY_DATA, team_id: "linked-team-id" };
renderWithProviders(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId="test-key-id" onKeyDataUpdate={() => {}} teams={[]} />,
);
expect(await screen.findByRole("link", { name: "Linked Team" })).toHaveAttribute(
"href",
expect.stringContaining("/teams?team=linked-team-id"),
);
});
it("links the key's user and creator to their user pages by id", async () => {
const keyData = {
...MOCK_KEY_DATA,
user_id: "owner-user-id",
user_email: "owner@example.com",
created_by: "creator-user-id",
created_by_user: { user_id: "creator-user-id", user_email: "creator@example.com", user_alias: null },
};
renderWithProviders(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId="test-key-id" onKeyDataUpdate={() => {}} teams={[]} />,
);
expect(await screen.findByRole("link", { name: "owner@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=owner-user-id"),
);
expect(screen.getByRole("link", { name: "creator@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=creator-user-id"),
);
});
it("links the key's organization by id, falling back to the team's organization", async () => {
const keyData = { ...MOCK_KEY_DATA, team_id: "linked-team-id", organization_id: null };
renderWithProviders(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId="test-key-id" onKeyDataUpdate={() => {}} teams={[]} />,
);
expect(await screen.findByRole("link", { name: "org-1" })).toHaveAttribute(
"href",
expect.stringContaining("/organizations?org=org-1"),
);
});
it("renders no team link when the key has no team", async () => {
renderWithProviders(
<KeyInfoView
keyData={{ ...MOCK_KEY_DATA, team_id: null }}
onClose={() => {}}
keyId="test-key-id"
onKeyDataUpdate={() => {}}
teams={[]}
/>,
);
await screen.findByText("Team");
expect(screen.queryByRole("link", { name: /team/i })).not.toBeInTheDocument();
});
});
it("should call onClose when back button is clicked", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const onCloseMock = vi.fn();

View file

@ -2,6 +2,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { ArrowLeft } from "lucide-react";
@ -10,6 +11,8 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EntityLink } from "@/components/shared/EntityLink";
import { teamDetailHref } from "@/utils/entityLinks";
import { KeyInfoHeader } from "./KeyInfoHeader";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
@ -78,6 +81,7 @@ export default function KeyInfoView({
const queryClient = useQueryClient();
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const { teams: teamsData } = useTeams();
const { data: organizations } = useOrganizations();
const { data: projects } = useProjects();
const { data: uiSettingsData } = useUISettings();
const { data: allMcpServers } = useMCPServers();
@ -453,6 +457,8 @@ export default function KeyInfoView({
const lastConfiguredAt = currentKeyData.settings_updated_at || currentKeyData.created_at;
const parentTeam = currentKeyData.team_id ? teamsData?.find((team) => team.team_id === currentKeyData.team_id) : null;
const orgId = currentKeyData.organization_id || currentKeyData.org_id || parentTeam?.organization_id || "";
const parentOrg = orgId ? organizations?.find((org) => org.organization_id === orgId) : null;
const budgetDisplay =
currentKeyData.max_budget !== null
@ -470,11 +476,16 @@ export default function KeyInfoView({
userId: currentKeyData.user_id || "",
userEmail: currentKeyData.user_email || "",
userAlias: currentKeyData.user?.user_alias ?? null,
teamId: currentKeyData.team_id || "",
teamAlias: parentTeam?.team_alias ?? null,
orgId,
orgAlias: parentOrg?.organization_alias ?? null,
createdBy:
currentKeyData.created_by_user?.user_alias ||
currentKeyData.created_by_user?.user_email ||
currentKeyData.created_by ||
"",
createdById: currentKeyData.created_by_user?.user_id || currentKeyData.created_by || "",
createdAt: currentKeyData.created_at ? formatTimestamp(currentKeyData.created_at) : "",
lastUpdated: lastConfiguredAt ? formatTimestamp(lastConfiguredAt) : "",
lastActive: currentKeyData.last_active ? formatTimestamp(currentKeyData.last_active) : "Never",
@ -766,7 +777,15 @@ export default function KeyInfoView({
<div>
<p className="text-sm font-medium">Team ID</p>
<p className="text-sm">{currentKeyData.team_id || "Not Set"}</p>
<p className="text-sm">
{currentKeyData.team_id ? (
<EntityLink href={teamDetailHref(currentKeyData.team_id)} className="font-normal">
{currentKeyData.team_id}
</EntityLink>
) : (
"Not Set"
)}
</p>
</div>
{enableProjectsUI && (

View file

@ -11,3 +11,7 @@ export function keyDetailHref(keyToken: string): string {
export function userDetailHref(userId: string): string {
return `${migratedHref("users")}?user=${encodeURIComponent(userId)}`;
}
export function orgDetailHref(orgId: string): string {
return `${migratedHref("organizations")}?org=${encodeURIComponent(orgId)}`;
}