diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index 0755ddb96fc..0910925a940 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -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", diff --git a/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx b/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx index a55381b344f..c211f256809 100644 --- a/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx @@ -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(); @@ -50,4 +52,34 @@ describe("LabeledField", () => { render(); expect(screen.getByRole("button", { name: "Copy User ID" })).toBeInTheDocument(); }); + + it("should render the value as a link when href is provided", () => { + render(); + 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(); + 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(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("should not link the Default Proxy Admin tag", () => { + render( + , + ); + expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument(); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx index 6107448babe..9f45b05f306 100644 --- a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx +++ b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx @@ -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 ? ( ) : ( - - {displayValue} - + {isLink ? ( + + {displayValue} + + ) : ( + + {displayValue} + + )} {isCopyable && } ); diff --git a/ui/litellm-dashboard/src/components/shared/BadgeLink.tsx b/ui/litellm-dashboard/src/components/shared/BadgeLink.tsx index 444d2acdcfe..06185249500 100644 --- a/ui/litellm-dashboard/src/components/shared/BadgeLink.tsx +++ b/ui/litellm-dashboard/src/components/shared/BadgeLink.tsx @@ -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 ( @@ -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 ( + + {children} + + ); +} + +function LinkedBadge({ href, variant, className, children }: BadgeLinkProps & { href: string }) { + const handleClick = useEntityLinkClick(href); return ( ({ useRouter: () => ({ push }) })); + +describe("EntityLink", () => { + beforeEach(() => { + push.mockClear(); + }); + + it("renders an anchor pointing at the target href", () => { + render(alice); + 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(alice); + 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(alice); + await user.keyboard("{Meta>}"); + await user.click(screen.getByRole("link", { name: "alice" })); + await user.keyboard("{/Meta}"); + expect(push).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/EntityLink.tsx b/ui/litellm-dashboard/src/components/shared/EntityLink.tsx new file mode 100644 index 00000000000..4054b929943 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/EntityLink.tsx @@ -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 ( + + {children} + + + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx index 62ae04bbf88..2ee49b18bc0 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx +++ b/ui/litellm-dashboard/src/components/shared/table_cells/identity_cell.tsx @@ -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 ( diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.test.tsx index f67f70e7df9..b81495e68e8 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.test.tsx @@ -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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument(); + expect(screen.queryByRole("link", { name: /default/i })).not.toBeInTheDocument(); + }); }); describe("back button", () => { diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx index f31a265da87..cffa356d693 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoHeader.tsx @@ -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
{displayValue}} + render={ + + {userId ? {displayValue} : displayValue} + + } /> {popoverContent} @@ -265,6 +278,7 @@ export function KeyInfoHeader({ label="Created By" value={data.createdBy} icon={} + href={data.createdById ? userDetailHref(data.createdById) : undefined} truncate copyable defaultUserIdCheck @@ -277,6 +291,25 @@ export function KeyInfoHeader({ } /> } />
+ + + +
+ } + href={data.teamId ? teamDetailHref(data.teamId) : undefined} + truncate + /> + } + href={data.orgId ? orgDetailHref(data.orgId) : undefined} + truncate + /> +
); diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx index 42d1884e563..983c91d87a7 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx @@ -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), }; diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.budget_display.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.budget_display.test.tsx index bab720f7517..1ffee10710f 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.budget_display.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.budget_display.test.tsx @@ -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: () =>
, })); @@ -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: [] }), diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index 1437689958c..0d41d199b22 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -13,6 +13,12 @@ const editViewMocks = vi.hoisted(() => ({ onSubmit: undefined as ((v: Record) => Promise) | 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) => Promise }) => { 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( + {}} 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( + {}} 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( + {}} 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( + {}} + 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(); diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index a2d926dff8a..3ee11325a65 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -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({

Team ID

-

{currentKeyData.team_id || "Not Set"}

+

+ {currentKeyData.team_id ? ( + + {currentKeyData.team_id} + + ) : ( + "Not Set" + )} +

{enableProjectsUI && ( diff --git a/ui/litellm-dashboard/src/utils/entityLinks.ts b/ui/litellm-dashboard/src/utils/entityLinks.ts index b0829d15d1d..675ac8d0554 100644 --- a/ui/litellm-dashboard/src/utils/entityLinks.ts +++ b/ui/litellm-dashboard/src/utils/entityLinks.ts @@ -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)}`; +}