feat(ui): link key info header to its user, creator, team, and organization (#37187)

* feat(ui): link key info header to its user, creator, team, and organization

The key info page showed the owning user and creator as plain text and never surfaced the team or organization at all, so walking from a key to its parent entities meant copying ids into other pages. The header now renders User and Created By as links to the user detail page, and gains a far-right column with Team and Organization links (alias when known, id otherwise). Client-side navigation logic shared by BadgeLink and IdentityCell moves into a reusable EntityLink so all entity links behave the same

* test(ui): mock next/navigation in VirtualKeysTable test

KeyInfoView now renders EntityLink, which calls useRouter, so the table test that opens the key detail needs the app router mocked
This commit is contained in:
ryan-crabbe-berri 2026-08-17 10:17:28 -07:00 committed by GitHub
parent 3c3ada9af0
commit 9d40cd4df7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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)}`;
}