feat(ui): add Expires to key Overview header; merge User into one field (#27696)

* fix(ui): resolve created_by to human-readable name for team keys

The team's Virtual Keys table rendered a raw UUID under Created By
for keys created on behalf of a real user, and the key details page
header showed "-" because it was reading the wrong field
(user_email/user_id instead of created_by_user).

- TeamVirtualKeysTable Created By column now prefers
  created_by_user.user_alias > user_email > UUID, with a Popover
  on hover that surfaces all three fields with copy icons
  (matches the existing AllKeys table pattern in VirtualKeysTable.tsx)
- key_info_view passes the resolved created_by_user value to the
  details-page header so it renders the readable name

Resolves LIT-2517

* fix(ui): add created_by to KeyResponse type

The backend returns created_by on every key row, but the frontend
type omitted it. The Created By cell already reads the field via
info.row.original, which trips the production typecheck.

* feat(ui): add Expires to key Overview header; merge User into one field

The key details page header omitted Expires (only Settings tab had it)
and showed User Email + User ID as two separate rows. This PR:

- adds Expires below Created At, reusing the Settings tab's
  formatTimestamp(...) ?? "Never" formatting so both views agree
- merges User Email / User ID into a single "User" field that
  displays alias / email / user_id (in that fallback order) with a
  Popover on hover exposing all three with copy icons — mirrors the
  Created By pattern in TeamVirtualKeysTable.tsx and VirtualKeysTable.tsx

Refs LIT-2517

* chore(ui): User field icon + alias-primary test

Address review feedback on #27696:
- Swap MailOutlined → UserOutlined on the merged User cell; the
  envelope icon implied "this is an email" but the cell can render
  alias / email / user_id depending on what's available.
- Add a test asserting userAlias displays as primary and overrides
  userEmail — closes the gap where a fallback-order regression
  would have silently passed.

* fix(ui): truncate long User values and reshuffle header layout

- Swap column groupings so Created By stays paired with Created At
  (matches the pre-merge layout); User and Expires now share col 1.
- Ellipsis-truncate the visible User cell at maxWidth 200 so a raw
  UUID fallback doesn't sprawl. Full identity still revealed via the
  hover Popover.
- Cap each Popover row at maxWidth 220 so a UUID truncates inside the
  panel too; antd's built-in ellipsis tooltip surfaces the full value.
This commit is contained in:
ryan-crabbe-berri 2026-05-12 10:54:55 -07:00 committed by GitHub
parent fc8a9a3406
commit b39cac9382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 179 additions and 27 deletions

View File

@ -53,6 +53,7 @@ export interface KeyResponse {
organization_id: string | null;
org_id?: string | null;
created_at: string;
created_by?: string;
updated_at: string;
last_active: string | null;
team_spend: number;

View File

@ -25,7 +25,8 @@ import {
Text,
} from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Popover, Skeleton, Tooltip } from "antd";
import { Popover, Skeleton, Tooltip, Typography } from "antd";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
@ -339,18 +340,59 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
size: 70,
enableSorting: false,
cell: (info) => {
const value = info.getValue() as string | null;
const displayValue = value === "default_user_id" ? "Default Proxy Admin" : value;
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 width = info.cell.column.getSize();
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-gray-400">{label}</span>
{value ? (
<Typography.Text
className="font-mono text-xs"
ellipsis={{ tooltip: value }}
copyable
>
{value}
</Typography.Text>
) : (
<span className="font-mono">-</span>
)}
</div>
))}
</div>
);
if (isDefaultAdmin && !userAlias && !userEmail) {
return (
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
<span className="cursor-default">
<DefaultProxyAdminTag userId={userId} />
</span>
</Popover>
);
}
return (
<Tooltip title={displayValue}>
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
<span
className="font-mono text-xs truncate block"
className="font-mono text-xs truncate block cursor-default"
style={{ maxWidth: width, overflow: "hidden" }}
>
{displayValue ?? "-"}
{displayValue}
</span>
</Tooltip>
</Popover>
);
},
},

View File

@ -8,10 +8,12 @@ const MOCK_DATA: KeyInfoData = {
keyId: "sk-1234567890abcdef",
userId: "user-abc-123",
userEmail: "test@example.com",
userAlias: null,
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",
expires: "Never",
};
describe("KeyInfoHeader", () => {
@ -28,12 +30,11 @@ describe("KeyInfoHeader", () => {
it("should render all metadata fields", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByText("User Email")).toBeInTheDocument();
expect(screen.getByText("User")).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("Expires")).toBeInTheDocument();
expect(screen.getByText("Last Updated")).toBeInTheDocument();
expect(screen.getByText("Last Active")).toBeInTheDocument();
});
@ -122,8 +123,8 @@ describe("KeyInfoHeader", () => {
});
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" };
it("should show Default Proxy Admin tag for User when userId is default_user_id and no alias/email", () => {
const data = { ...MOCK_DATA, userId: "default_user_id", userEmail: "", userAlias: null };
render(<KeyInfoHeader data={data} />);
expect(screen.getAllByText("Default Proxy Admin").length).toBeGreaterThanOrEqual(1);
});
@ -135,9 +136,27 @@ describe("KeyInfoHeader", () => {
});
});
describe("empty value handling", () => {
it("should show '-' for User Email when value is empty", () => {
const data = { ...MOCK_DATA, userEmail: "" };
describe("User field fallbacks", () => {
it("should display userAlias as primary when set, overriding email and userId", () => {
const data = { ...MOCK_DATA, userAlias: "alice" };
render(<KeyInfoHeader data={data} />);
expect(screen.getByText("alice")).toBeInTheDocument();
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
});
it("should display userEmail when alias is null", () => {
render(<KeyInfoHeader data={MOCK_DATA} />);
expect(screen.getByText("test@example.com")).toBeInTheDocument();
});
it("should fall back to userId when alias and email are missing", () => {
const data = { ...MOCK_DATA, userEmail: "", userAlias: null };
render(<KeyInfoHeader data={data} />);
expect(screen.getByText("user-abc-123")).toBeInTheDocument();
});
it("should show '-' when alias, email, and userId are all empty", () => {
const data = { ...MOCK_DATA, userId: "", userEmail: "", userAlias: null };
render(<KeyInfoHeader data={data} />);
expect(screen.getByText("-")).toBeInTheDocument();
});

View File

@ -1,19 +1,20 @@
import React from "react";
import { Button, Typography, Tooltip, Space, Divider, Flex } from "antd";
import { Button, Typography, Tooltip, Space, Divider, Flex, Popover } from "antd";
import {
ArrowLeftOutlined,
SyncOutlined,
DeleteOutlined,
PlusOutlined,
UserOutlined,
MailOutlined,
CalendarOutlined,
ClockCircleOutlined,
ThunderboltOutlined,
SafetyCertificateOutlined,
TransactionOutlined,
FieldTimeOutlined,
} from "@ant-design/icons";
import LabeledField from "../common_components/LabeledField";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
const { Title, Text } = Typography;
@ -22,10 +23,12 @@ export interface KeyInfoData {
keyId: string;
userId: string;
userEmail: string;
userAlias?: string | null;
createdBy: string;
createdAt: string;
lastUpdated: string;
lastActive: string;
expires: string;
}
interface KeyInfoHeaderProps {
@ -41,6 +44,94 @@ interface KeyInfoHeaderProps {
regenerateTooltip?: string;
}
function UserField({
userAlias,
userEmail,
userId,
}: {
userAlias?: string | null;
userEmail: string;
userId: string;
}) {
const labelEl = (
<Space size={4}>
<Text type="secondary"><UserOutlined /></Text>
<Text type="secondary" style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.05em" }}>
User
</Text>
</Space>
);
const isEmpty = !userAlias && !userEmail && !userId;
if (isEmpty) {
return (
<div>
{labelEl}
<div><Text strong>-</Text></div>
</div>
);
}
const isDefaultAdmin = userId === "default_user_id";
const displayValue = userAlias || userEmail || userId;
const popoverContent = (
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
{[
{ label: "User Alias", value: userAlias ?? null },
{ label: "User Email", value: userEmail || null },
{ label: "User ID", value: userId || null },
].map(({ label, value }) => (
<div key={label} className="flex flex-col min-w-0">
<span className="text-gray-400">{label}</span>
{value ? (
<Typography.Text
className="font-mono text-xs"
style={{ maxWidth: 220 }}
ellipsis={{ tooltip: value }}
copyable
>
{value}
</Typography.Text>
) : (
<span className="font-mono">-</span>
)}
</div>
))}
</div>
);
if (isDefaultAdmin && !userAlias && !userEmail) {
return (
<div>
{labelEl}
<div>
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
<span className="cursor-default"><DefaultProxyAdminTag userId={userId} /></span>
</Popover>
</div>
</div>
);
}
return (
<div>
{labelEl}
<div>
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
<Text
strong
ellipsis
style={{ cursor: "default", maxWidth: 200, display: "block" }}
>
{displayValue}
</Text>
</Popover>
</div>
</div>
);
}
export function KeyInfoHeader({
data,
onBack,
@ -101,15 +192,8 @@ export function KeyInfoHeader({
<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
/>
<UserField userAlias={data.userAlias} userEmail={data.userEmail} userId={data.userId} />
<LabeledField label="Expires" value={data.expires} icon={<FieldTimeOutlined />} />
</Space>
<Divider type="vertical" style={{ height: "auto" }} />

View File

@ -403,10 +403,16 @@ export default function KeyInfoView({
keyId: currentKeyData.token_id || currentKeyData.token,
userId: currentKeyData.user_id || "",
userEmail: currentKeyData.user_email || "",
createdBy: currentKeyData.user_email || currentKeyData.user_id || "",
userAlias: currentKeyData.user?.user_alias ?? null,
createdBy:
currentKeyData.created_by_user?.user_alias ||
currentKeyData.created_by_user?.user_email ||
currentKeyData.created_by ||
"",
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",
expires: currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never",
}}
onBack={onClose}
onRegenerate={() => setIsRegenerateModalOpen(true)}