[Feature] UI - Virtual Keys: Add KeyInfoHeader component with metadata display

Add a reusable KeyInfoHeader component to replace the inline header in KeyInfoView.
Extract LabeledField as a common component for labeled metadata display with copyable
support, default_user_id handling, and empty value placeholders.

- Migrate all icons from lucide-react/heroicons to @ant-design/icons
- Use antd native copyable with descriptive tooltips (Copy Key Alias, Copy Key ID, etc.)
- Show DefaultProxyAdminTag for default_user_id values
- Add canModifyKey, regenerateDisabled, regenerateTooltip props for permission gating
- Add tests for KeyInfoHeader (19 tests) and LabeledField (8 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-02-24 16:46:26 -08:00 committed by Sameer Kankute
parent 28a225d8bd
commit fd05c49787
6 changed files with 414 additions and 92 deletions

View file

@ -453,8 +453,8 @@ it("should open KeyInfoView when clicking on a key ID button", async () => {
// Wait for KeyInfoView to appear - check for unique elements that only exist in KeyInfoView
await waitFor(() => {
expect(screen.getByText("Back to Keys")).toBeInTheDocument();
// KeyInfoView shows "Created:" or "Updated:" which is unique to it
expect(screen.getByText(/Created:|Updated:/)).toBeInTheDocument();
// KeyInfoHeader shows "Created At" metadata label
expect(screen.getByText("Created At")).toBeInTheDocument();
});
// Verify that table-specific elements are no longer visible

View file

@ -0,0 +1,57 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import LabeledField from "./LabeledField";
describe("LabeledField", () => {
it("should render the label and value", () => {
render(<LabeledField label="User Email" value="test@example.com" />);
expect(screen.getByText("User Email")).toBeInTheDocument();
expect(screen.getByText("test@example.com")).toBeInTheDocument();
});
it("should render the icon when provided", () => {
render(
<LabeledField label="Name" value="Alice" icon={<span data-testid="test-icon" />} />,
);
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
});
it("should show '-' when value is empty", () => {
render(<LabeledField label="User ID" value="" />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("should show 'Default Proxy Admin' tag when value is default_user_id and defaultUserIdCheck is true", () => {
render(
<LabeledField label="User ID" value="default_user_id" copyable defaultUserIdCheck />,
);
expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
expect(screen.queryByText("default_user_id")).not.toBeInTheDocument();
});
it("should show raw value when value is default_user_id but defaultUserIdCheck is false", () => {
render(<LabeledField label="User ID" value="default_user_id" />);
expect(screen.getByText("default_user_id")).toBeInTheDocument();
expect(screen.queryByText("Default Proxy Admin")).not.toBeInTheDocument();
});
it("should not be copyable when value is empty", () => {
const { container } = render(<LabeledField label="User ID" value="" copyable />);
// antd adds a .ant-typography-copy element when copyable; should not be present
expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument();
});
it("should not be copyable when value is default_user_id and defaultUserIdCheck is true", () => {
const { container } = render(
<LabeledField label="User ID" value="default_user_id" copyable defaultUserIdCheck />,
);
expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument();
});
it("should be copyable when copyable is true and value is present", () => {
const { container } = render(
<LabeledField label="User ID" value="user-123" copyable />,
);
expect(container.querySelector(".ant-typography-copy")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,52 @@
import React from "react";
import { Typography, Space } from "antd";
import DefaultProxyAdminTag from "./DefaultProxyAdminTag";
const { Text } = Typography;
interface LabeledFieldProps {
label: string;
value: string;
icon?: React.ReactNode;
truncate?: boolean;
copyable?: boolean;
defaultUserIdCheck?: boolean;
}
export default function LabeledField({
label,
value,
icon,
truncate = false,
copyable = false,
defaultUserIdCheck = false,
}: LabeledFieldProps) {
const isEmpty = !value;
const isDefaultUser = defaultUserIdCheck && value === "default_user_id";
const displayValue = isEmpty ? "-" : value;
const isCopyable = copyable && !isEmpty && !isDefaultUser;
const valueEl = isDefaultUser ? (
<DefaultProxyAdminTag userId={value} />
) : (
<Text
strong
copyable={isCopyable ? { tooltips: [`Copy ${label}`, "Copied!"] } : false}
ellipsis={truncate}
style={truncate ? { maxWidth: 160, display: "block" } : undefined}
>
{displayValue}
</Text>
);
return (
<div>
<Space size={4}>
<Text type="secondary">{icon}</Text>
<Text type="secondary" style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{label}
</Text>
</Space>
<div>{valueEl}</div>
</div>
);
}

View file

@ -0,0 +1,145 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { KeyInfoHeader, KeyInfoData } from "./KeyInfoHeader";
const MOCK_DATA: KeyInfoData = {
keyName: "My Test Key",
keyId: "sk-1234567890abcdef",
userId: "user-abc-123",
userEmail: "test@example.com",
createdBy: "admin@example.com",
createdAt: "Oct 29, 2025 at 1:26 AM",
lastUpdated: "Oct 29, 2025 at 1:47 AM",
lastActive: "Oct 29, 2025 at 2:00 AM",
};
describe("KeyInfoHeader", () => {
it("should render", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByText("My Test Key")).toBeInTheDocument();
});
it("should render the key ID with prefix", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByText(/Key ID:/)).toBeInTheDocument();
expect(screen.getByText(/sk-1234567890abcdef/)).toBeInTheDocument();
});
it("should render all metadata fields", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByText("User Email")).toBeInTheDocument();
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getByText("User ID")).toBeInTheDocument();
expect(screen.getByText("user-abc-123")).toBeInTheDocument();
expect(screen.getByText("Created At")).toBeInTheDocument();
expect(screen.getByText("Created By")).toBeInTheDocument();
expect(screen.getByText("Last Updated")).toBeInTheDocument();
expect(screen.getByText("Last Active")).toBeInTheDocument();
});
describe("back button", () => {
it("should render with default text", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("button", { name: /back to keys/i })).toBeInTheDocument();
});
it("should render with custom text", () => {
render(<KeyInfoHeader data={MOCK_DATA} backButtonText="Back to Dashboard" />);
expect(screen.getByRole("button", { name: /back to dashboard/i })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /back to keys/i })).not.toBeInTheDocument();
});
it("should call onBack when clicked", async () => {
const onBack = vi.fn();
render(<KeyInfoHeader data={MOCK_DATA} onBack={onBack} />);
await userEvent.click(screen.getByRole("button", { name: /back to keys/i }));
expect(onBack).toHaveBeenCalledTimes(1);
});
});
describe("action buttons", () => {
it("should show Regenerate and Delete buttons by default", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("button", { name: /regenerate key/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /delete key/i })).toBeInTheDocument();
});
it("should show Regenerate and Delete buttons when canModifyKey is true", () => {
render(<KeyInfoHeader data={MOCK_DATA} canModifyKey={true} />);
expect(screen.getByRole("button", { name: /regenerate key/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /delete key/i })).toBeInTheDocument();
});
it("should hide Regenerate and Delete buttons when canModifyKey is false", () => {
render(<KeyInfoHeader data={MOCK_DATA} canModifyKey={false} />);
expect(screen.queryByRole("button", { name: /regenerate key/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /delete key/i })).not.toBeInTheDocument();
});
it("should call onRegenerate when Regenerate Key is clicked", async () => {
const onRegenerate = vi.fn();
render(<KeyInfoHeader data={MOCK_DATA} onRegenerate={onRegenerate} />);
await userEvent.click(screen.getByRole("button", { name: /regenerate key/i }));
expect(onRegenerate).toHaveBeenCalledTimes(1);
});
it("should call onDelete when Delete Key is clicked", async () => {
const onDelete = vi.fn();
render(<KeyInfoHeader data={MOCK_DATA} onDelete={onDelete} />);
await userEvent.click(screen.getByRole("button", { name: /delete key/i }));
expect(onDelete).toHaveBeenCalledTimes(1);
});
it("should disable Regenerate button when regenerateDisabled is true", () => {
render(<KeyInfoHeader data={MOCK_DATA} regenerateDisabled={true} />);
expect(screen.getByRole("button", { name: /regenerate key/i })).toBeDisabled();
});
it("should not disable Regenerate button by default", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByRole("button", { name: /regenerate key/i })).not.toBeDisabled();
});
});
describe("Create New Key button", () => {
it("should show when onCreateNew is provided", () => {
render(<KeyInfoHeader data={MOCK_DATA} onCreateNew={vi.fn()} />);
expect(screen.getByRole("button", { name: /create new key/i })).toBeInTheDocument();
});
it("should hide when onCreateNew is not provided", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.queryByRole("button", { name: /create new key/i })).not.toBeInTheDocument();
});
it("should call onCreateNew when clicked", async () => {
const onCreateNew = vi.fn();
render(<KeyInfoHeader data={MOCK_DATA} onCreateNew={onCreateNew} />);
await userEvent.click(screen.getByRole("button", { name: /create new key/i }));
expect(onCreateNew).toHaveBeenCalledTimes(1);
});
});
describe("default_user_id handling", () => {
it("should show Default Proxy Admin tag for User ID when value is default_user_id", () => {
const data = { ...MOCK_DATA, userId: "default_user_id" };
render(<KeyInfoHeader data={data} />);
expect(screen.getAllByText("Default Proxy Admin").length).toBeGreaterThanOrEqual(1);
});
it("should show Default Proxy Admin tag for Created By when value is default_user_id", () => {
const data = { ...MOCK_DATA, createdBy: "default_user_id" };
render(<KeyInfoHeader data={data} />);
expect(screen.getAllByText("Default Proxy Admin").length).toBeGreaterThanOrEqual(1);
});
});
describe("empty value handling", () => {
it("should show '-' for User Email when value is empty", () => {
const data = { ...MOCK_DATA, userEmail: "" };
render(<KeyInfoHeader data={data} />);
expect(screen.getByText("-")).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,130 @@
import React from "react";
import { Button, Typography, Tooltip, Space, Divider, Flex } from "antd";
import {
ArrowLeftOutlined,
SyncOutlined,
DeleteOutlined,
PlusOutlined,
UserOutlined,
MailOutlined,
CalendarOutlined,
ClockCircleOutlined,
ThunderboltOutlined,
SafetyCertificateOutlined,
} from "@ant-design/icons";
import LabeledField from "../common_components/LabeledField";
const { Title, Text } = Typography;
export interface KeyInfoData {
keyName: string;
keyId: string;
userId: string;
userEmail: string;
createdBy: string;
createdAt: string;
lastUpdated: string;
lastActive: string;
}
interface KeyInfoHeaderProps {
data: KeyInfoData;
onBack?: () => void;
onCreateNew?: () => void;
onRegenerate?: () => void;
onDelete?: () => void;
canModifyKey?: boolean;
backButtonText?: string;
regenerateDisabled?: boolean;
regenerateTooltip?: string;
}
export function KeyInfoHeader({
data,
onBack,
onCreateNew,
onRegenerate,
onDelete,
canModifyKey = true,
backButtonText = "Back to Keys",
regenerateDisabled = false,
regenerateTooltip,
}: KeyInfoHeaderProps) {
return (
<div>
{onCreateNew && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={onCreateNew}>
Create New Key
</Button>
</div>
)}
<div style={{ marginBottom: 16 }}>
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack}>
{backButtonText}
</Button>
</div>
<Flex justify="space-between" align="start" style={{ marginBottom: 20 }}>
<div>
<Title level={3} copyable={{ tooltips: ["Copy Key Alias", "Copied!"] }} style={{ margin: 0 }}>
{data.keyName}
</Title>
<Text type="secondary" copyable={{ text: data.keyId, tooltips: ["Copy Key ID", "Copied!"] }}>
Key ID: {data.keyId}
</Text>
</div>
{canModifyKey && (
<Space>
<Tooltip title={regenerateTooltip || ""}>
<span>
<Button icon={<SyncOutlined />} onClick={onRegenerate} disabled={regenerateDisabled}>
Regenerate Key
</Button>
</span>
</Tooltip>
<Button danger icon={<DeleteOutlined />} onClick={onDelete}>
Delete Key
</Button>
</Space>
)}
</Flex>
<Flex align="stretch" gap={40} style={{ marginBottom: 40 }}>
<Space direction="vertical" size={16}>
<LabeledField label="User Email" value={data.userEmail} icon={<MailOutlined />} />
<LabeledField
label="User ID"
value={data.userId}
icon={<UserOutlined />}
truncate
copyable
defaultUserIdCheck
/>
</Space>
<Divider type="vertical" style={{ height: "auto" }} />
<Space direction="vertical" size={16}>
<LabeledField label="Created At" value={data.createdAt} icon={<CalendarOutlined />} />
<LabeledField
label="Created By"
value={data.createdBy}
icon={<SafetyCertificateOutlined />}
truncate
copyable
defaultUserIdCheck
/>
</Space>
<Divider type="vertical" style={{ height: "auto" }} />
<Space direction="vertical" size={16}>
<LabeledField label="Last Updated" value={data.lastUpdated} icon={<ClockCircleOutlined />} />
<LabeledField label="Last Active" value={data.lastActive} icon={<ThunderboltOutlined />} />
</Space>
</Flex>
</div>
);
}

View file

@ -1,13 +1,13 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { ArrowLeftIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
import { Button as AntdButton, Form, Tag, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Form, Tag } from "antd";
import { KeyInfoHeader } from "./KeyInfoHeader";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "../../utils/roles";
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
import AutoRotationView from "../common_components/AutoRotationView";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
@ -54,8 +54,6 @@ export default function KeyInfoView({
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
// Add local state to maintain key data and track regeneration
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData);
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(null);
@ -284,16 +282,6 @@ export default function KeyInfoView({
}
};
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
}
};
const handleRegenerateKeyUpdate = (updatedKeyData: Partial<KeyResponse>) => {
// Update local state immediately with ALL the new data
setCurrentKeyData((prevData) => {
@ -346,79 +334,29 @@ export default function KeyInfoView({
return (
<div className="w-full h-screen p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
{backButtonText}
</Button>
<Title>{currentKeyData.key_alias || "Virtual Key"}</Title>
<div className="flex items-center cursor-pointer mb-2 space-y-6">
<div>
<Text className="text-xs text-gray-400 uppercase tracking-wide mt-2">Key ID</Text>
<Text className="text-gray-500 font-mono text-sm">{currentKeyData.token_id || currentKeyData.token}</Text>
</div>
<AntdButton
type="text"
size="small"
icon={copiedStates["key-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(currentKeyData.token_id || currentKeyData.token, "key-id")}
className={`ml-2 transition-all duration-200${copiedStates["key-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
{/* Add timestamp and regeneration indicator */}
<div className="flex items-center gap-2 flex-wrap">
<Text className="text-sm text-gray-500">
{currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at
? `Updated: ${formatTimestamp(currentKeyData.updated_at)}`
: `Created: ${formatTimestamp(currentKeyData.created_at)}`}
</Text>
{isRecentlyRegenerated && (
<Badge color="green" size="xs" className="animate-pulse">
Recently Regenerated
</Badge>
)}
{lastRegeneratedAt && (
<Badge color="blue" size="xs">
Regenerated
</Badge>
)}
</div>
</div>
{canModifyKey && (
<div className="flex gap-2">
<Tooltip
title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}
>
<span className="inline-block">
<Button
icon={RefreshIcon}
variant="secondary"
onClick={() => setIsRegenerateModalOpen(true)}
className="flex items-center"
disabled={!premiumUser}
>
Regenerate Key
</Button>
</span>
</Tooltip>
<Button
icon={TrashIcon}
variant="secondary"
onClick={() => setIsDeleteModalOpen(true)}
className="flex items-center text-red-500 border-red-500 hover:text-red-700"
>
Delete Key
</Button>
</div>
)}
</div>
<KeyInfoHeader
data={{
keyName: currentKeyData.key_alias || "Virtual Key",
keyId: currentKeyData.token_id || currentKeyData.token,
userId: currentKeyData.user_id || "",
userEmail: currentKeyData.user_email || "",
createdBy: currentKeyData.user_email || currentKeyData.user_id || "",
createdAt: currentKeyData.created_at ? formatTimestamp(currentKeyData.created_at) : "",
lastUpdated: currentKeyData.updated_at ? formatTimestamp(currentKeyData.updated_at) : "",
lastActive: currentKeyData.last_active ? formatTimestamp(currentKeyData.last_active) : "Never",
}}
onBack={onClose}
onRegenerate={() => setIsRegenerateModalOpen(true)}
onDelete={() => setIsDeleteModalOpen(true)}
canModifyKey={canModifyKey}
backButtonText={backButtonText}
regenerateDisabled={!premiumUser}
regenerateTooltip={
!premiumUser
? "This is a LiteLLM Enterprise feature, and requires a valid key to use."
: undefined
}
/>
{/* Add RegenerateKeyModal */}
<RegenerateKeyModal