feat(ui): rebuild the Virtual Keys table on the shared DataTable (#32991)

* feat(ui): rebuild the Virtual Keys table on the shared DataTable

Replaces the hand-rolled Tremor table and bespoke toolbar/pagination on the admin
Virtual Keys page with the shared DataTable: server-side sort, paginate, and
filter, a sticky scrolling body, a search plus column-visibility plus filters
toolbar, a right-side filter drawer, and a rows-per-page footer. A page header
with the existing key icon carries the Create New Key action.

Adds reusable, shadcn-default building blocks for the tables migrating onto the
DataTable next: shared IdentityCell, ModelsCell, and SpendBudgetCell in
shared/table_cells, plus a shared PageHeader. The models cell reveals overflow in
a hover tooltip and the spend/budget cell uses the Meter primitive.

All data and domain logic is preserved, including the useKeys query, team and org
alias resolution, the user popover, and the KeyInfoView detail swap. The rich
async Team/Org/Alias filters move into the drawer, and the toolbar search maps to
the key-alias substring search. Status now also reflects key expiry alongside
blocked and SCIM-blocked.

The VirtualKeysTable tests are updated to the new markup and extended with focused
coverage for each new shared cell

* fix(ui): address Virtual Keys redesign review feedback

Fold the status badge into the clickable Key cell and drop the separate Status
column so a key's alias, secret, and status read as one unit. The Key cell is
now the single click target that opens the key detail; the whole-row click is
removed

Migrate the filter drawer off AntD to shadcn. A new Combobox composed from
Popover and Input backs the Team, Organization, and Key Alias filters, keeping
search and the alias infinite-scroll

Show $0.00 for zero spend instead of a hyphen, and extend the shared DataTable
with badge, chips, and meter skeleton shapes so the loading state matches the
loaded cells (status pill, model chips, spend meter) rather than uniform bars

Fix key sorting: the Key column sent its column id "key" as sort_by, which
/key/list rejects with 400. It now sorts by the backend field key_alias

* fix(ui): use the shadcn base combobox and refine the keys filters and skeletons

Replace the hand-rolled filter combobox with the supported shadcn Base UI combobox
(ui/combobox, added via the CLI and reused through a small SearchSelect wrapper).
Its vended input-group and textarea deps are written for React 19 (plain functions
with ref-as-prop); this app is on React 18, where those subcomponents drop the refs
Base UI passes for focus and anchoring, so InputGroupInput, InputGroupButton, and
ComboboxTrigger are adapted to forwardRef. Those ui/ files now diverge from the
registry, and a future shadcn add would overwrite the adaptation until the app moves
to React 19. Adds class-variance-authority, which input-group needs

Give loading skeletons a per-column renderSkeleton escape hatch on the shared
DataTable and mirror the Key cell exactly (alias line, secret, status pill), so
skeleton rows match the real rows instead of being shorter and simpler

Resolve the automated review: the toolbar search and the drawer Key Alias filter
both mapped to the key-alias query, so the search silently overrode the drawer value
while its chip stayed visible. Consolidate to a single alias search in the toolbar
(placeholder now "Search by key alias…") and drop the redundant drawer field. Re-add
coverage for the Created By column's alias-over-email display

Refine the Team and Organization filters: they match on name and id, so the labels
read "Team" and "Organization" rather than "... ID", each option shows the name with
the id on a muted second line instead of "name (id)", and the active-filter chip
shows the friendly name

* chore(ui): drop duplicate class-variance-authority, use the repo cva package in input-group
This commit is contained in:
yuneng-jiang 2026-07-13 12:49:01 -07:00 committed by GitHub
parent aa9dcb43cf
commit fa09cde3c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1647 additions and 890 deletions

View file

@ -1690,14 +1690,6 @@
"count": 1
}
},
"src/components/VirtualKeysPage/VirtualKeysTable.tsx": {
"no-nested-ternary": {
"count": 2
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/activity_metrics.tsx": {
"no-nested-ternary": {
"count": 1

View file

@ -63,7 +63,7 @@ const mockKey: KeyResponse = {
key_alias: "Test Key Alias",
spend: 5.5,
max_budget: 100,
expires: "2024-12-31T23:59:59Z",
expires: "2999-12-31T23:59:59Z",
models: ["gpt-3.5-turbo", "gpt-4"],
aliases: {},
config: {},
@ -154,6 +154,8 @@ const keysResult = (keys: KeyResponse[], data: Partial<KeysResponse> = {}, extra
...extra,
}) as any;
const openFilters = () => fireEvent.click(screen.getByRole("button", { name: "Filters" }));
beforeEach(() => {
vi.clearAllMocks();
@ -170,6 +172,12 @@ it("should render VirtualKeysTable component", () => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
});
it("renders the page header with the create-key action slot", () => {
renderWithProviders(<VirtualKeysTable headerActions={<button>Create New Key</button>} />);
expect(screen.getByRole("heading", { name: "Virtual Keys" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create New Key" })).toBeInTheDocument();
});
it("should display key information correctly", async () => {
renderWithProviders(<VirtualKeysTable />);
@ -177,6 +185,7 @@ it("should display key information correctly", async () => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
expect(screen.getByText("Test Team")).toBeInTheDocument();
expect(screen.getByText("$5.5000")).toBeInTheDocument();
expect(screen.getByText("of $100")).toBeInTheDocument();
});
});
@ -188,14 +197,49 @@ it("should display user email correctly", async () => {
});
});
it("should show loading message only on initial load (isPending)", () => {
it("shows the user alias over the email in the visible cell when both exist", async () => {
mockUseKeys.mockReturnValue(
keysResult([{ ...mockKey, user: { user_id: "user-1", user_email: "user@example.com", user_alias: "The User" } }]),
);
renderWithProviders(<VirtualKeysTable />);
const row = (await screen.findByText("Test Key Alias")).closest("tr") as HTMLElement;
expect(within(row).getByText("The User")).toBeInTheDocument();
expect(within(row).queryByText("user@example.com")).not.toBeInTheDocument();
});
it("shows created_by_user alias over email in the Created By column when it is enabled", async () => {
mockUseKeys.mockReturnValue(
keysResult([
{
...mockKey,
created_by: "some-uuid",
created_by_user: { user_id: "some-uuid", user_email: "creator@example.com", user_alias: "The Creator" },
},
]),
);
const user = userEvent.setup();
renderWithProviders(<VirtualKeysTable />);
// Created By is hidden by default; turn it on via the Columns menu.
await user.click(screen.getByRole("button", { name: "Columns" }));
await user.click(await screen.findByText("Created By"));
await user.keyboard("{Escape}");
const row = (await screen.findByText("Test Key Alias")).closest("tr") as HTMLElement;
expect(within(row).getByText("The Creator")).toBeInTheDocument();
expect(within(row).queryByText("creator@example.com")).not.toBeInTheDocument();
});
it("should show a loading state on the initial load and hide the data", () => {
mockUseKeys.mockReturnValue(keysResult([], {}, { data: null, isPending: true, isFetching: true }));
renderWithProviders(<VirtualKeysTable />);
expect(screen.getByText("🚅 Loading keys...")).toBeInTheDocument();
expect(screen.getByText("Loading keys...")).toBeInTheDocument();
expect(screen.getAllByTestId("skeleton-row").length).toBeGreaterThan(0);
expect(screen.queryByText("Test Key Alias")).not.toBeInTheDocument();
expect(screen.queryByText("Test Team")).not.toBeInTheDocument();
});
it("should show 'No keys found' message when the key list is empty", () => {
@ -206,61 +250,52 @@ it("should show 'No keys found' message when the key list is empty", () => {
expect(screen.getByText("No keys found")).toBeInTheDocument();
});
it("should handle models with more than 3 entries to trigger expansion UI", () => {
it("collapses models beyond the visible limit into a '+N more' badge", () => {
mockUseKeys.mockReturnValue(
keysResult([{ ...mockKey, models: ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "claude-3", "claude-3-5-sonnet"] }]),
);
renderWithProviders(<VirtualKeysTable />);
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
expect(screen.getByText("+2 more")).toBeInTheDocument();
});
it("should render table headers correctly", () => {
it("should render the redesigned table headers", () => {
renderWithProviders(<VirtualKeysTable />);
expect(screen.getByText("Key ID")).toBeInTheDocument();
expect(screen.getByText("Key Alias")).toBeInTheDocument();
expect(screen.getByText("Key")).toBeInTheDocument();
expect(screen.getByText("Team")).toBeInTheDocument();
expect(screen.getByText("Models")).toBeInTheDocument();
expect(screen.getByText("Spend (USD)")).toBeInTheDocument();
expect(screen.getByText("Spend / Budget")).toBeInTheDocument();
});
it("should handle column resizing hover events", () => {
it("sorts by the backend key_alias field (not the column label) when the Key header is clicked", async () => {
renderWithProviders(<VirtualKeysTable />);
const headerCell = document.querySelector("[data-header-id]") as HTMLElement;
expect(headerCell).toBeInTheDocument();
const keyHeader = screen.getByText("Key").closest("button") as HTMLElement;
fireEvent.click(keyHeader);
const resizer = headerCell?.querySelector(".resizer") as HTMLElement;
expect(resizer).toBeInTheDocument();
expect(resizer.style.opacity).toBe("0");
fireEvent.mouseEnter(headerCell);
expect(resizer.style.opacity).toBe("0.5");
fireEvent.mouseLeave(headerCell);
expect(resizer.style.opacity).toBe("0");
await waitFor(() => {
expect(mockUseKeys).toHaveBeenLastCalledWith(1, 50, expect.objectContaining({ sortBy: "key_alias" }));
});
});
it("should open KeyInfoView when clicking on a key ID button", async () => {
it("should open KeyInfoView when clicking the key cell", async () => {
renderWithProviders(<VirtualKeysTable />);
await waitFor(() => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
});
expect(screen.getByText(/Showing.*results/)).toBeInTheDocument();
expect(screen.getByTestId("pagination-range")).toBeInTheDocument();
const keyIdButton = screen.getByText("sk-1234567890abcdef");
fireEvent.click(keyIdButton);
fireEvent.click(screen.getByText("Test Key Alias"));
await waitFor(() => {
expect(screen.getByText("Back to Keys")).toBeInTheDocument();
expect(screen.getByText("Created At")).toBeInTheDocument();
});
expect(screen.queryByText(/Showing.*results/)).not.toBeInTheDocument();
expect(screen.queryByTestId("pagination-range")).not.toBeInTheDocument();
});
it("should display 'Default Proxy Admin' for user_id when value is 'default_user_id'", async () => {
@ -282,44 +317,6 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user
});
});
it("should display created_by_user email in 'Created By' column when available", async () => {
mockUseKeys.mockReturnValue(
keysResult([
{
...mockKey,
created_by: "some-uuid-1234",
created_by_user: { user_id: "some-uuid-1234", user_email: "creator@example.com", user_alias: null },
},
]),
);
renderWithProviders(<VirtualKeysTable />);
await waitFor(() => {
expect(screen.getByText("creator@example.com")).toBeInTheDocument();
});
});
it("should display created_by_user alias over email when both are available", async () => {
mockUseKeys.mockReturnValue(
keysResult([
{
...mockKey,
created_by: "some-uuid-1234",
created_by_user: { user_id: "some-uuid-1234", user_email: "creator@example.com", user_alias: "The Creator" },
},
]),
);
renderWithProviders(<VirtualKeysTable />);
// Scope to the key's row so we assert the visible cell value: the hover popover that
// also holds the email is portaled out of the row, not the displayed "Created By" text.
const row = (await screen.findByText("Test Key Alias")).closest("tr") as HTMLElement;
expect(within(row).getByText("The Creator")).toBeInTheDocument();
expect(within(row).queryByText("creator@example.com")).not.toBeInTheDocument();
});
it("should render table without crashing when models is null", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, models: null as unknown as string[] }]));
@ -327,6 +324,7 @@ it("should render table without crashing when models is null", async () => {
await waitFor(() => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
});
@ -341,13 +339,14 @@ it("should display 'Unknown' for last_active when value is null", async () => {
});
describe("server-side filtering the LIT-4080 regression guard", () => {
it("threads an active User ID filter into the useKeys query so any refetch keeps it", async () => {
it("threads an applied User ID filter into the useKeys query so any refetch keeps it", async () => {
renderWithProviders(<VirtualKeysTable />);
fireEvent.click(screen.getByRole("button", { name: "Filters" }));
openFilters();
const userIdInput = await screen.findByPlaceholderText("Enter User ID...");
const userIdInput = await screen.findByPlaceholderText(/Enter User ID/);
fireEvent.change(userIdInput, { target: { value: "user-42" } });
fireEvent.click(screen.getByTestId("filter-drawer-apply"));
await waitFor(() => {
expect(mockUseKeys).toHaveBeenLastCalledWith(1, 50, expect.objectContaining({ userID: "user-42" }));
@ -361,18 +360,19 @@ describe("server-side filtering the LIT-4080 regression guard", () => {
expect(lastCall[2] ?? {}).toMatchObject({ userID: undefined, teamID: undefined, keyHash: undefined });
});
it("drops the filter from the useKeys query when Reset Filters is clicked", async () => {
it("drops the filter from the useKeys query when it is cleared", async () => {
renderWithProviders(<VirtualKeysTable />);
fireEvent.click(screen.getByRole("button", { name: "Filters" }));
const userIdInput = await screen.findByPlaceholderText("Enter User ID...");
openFilters();
const userIdInput = await screen.findByPlaceholderText(/Enter User ID/);
fireEvent.change(userIdInput, { target: { value: "user-42" } });
fireEvent.click(screen.getByTestId("filter-drawer-apply"));
await waitFor(() => {
expect(mockUseKeys).toHaveBeenLastCalledWith(1, 50, expect.objectContaining({ userID: "user-42" }));
});
fireEvent.click(screen.getByRole("button", { name: "Reset Filters" }));
fireEvent.click(screen.getByTestId("datatable-clear-filters"));
await waitFor(() => {
const lastCall = mockUseKeys.mock.calls[mockUseKeys.mock.calls.length - 1];
@ -388,8 +388,8 @@ describe("pagination display total count comes from useKeys", () => {
renderWithProviders(<VirtualKeysTable />);
await waitFor(() => {
expect(screen.getByText("Showing 1 - 50 of 509 results")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 11")).toBeInTheDocument();
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-50 of 509");
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 11");
});
});
@ -399,57 +399,44 @@ describe("pagination display total count comes from useKeys", () => {
renderWithProviders(<VirtualKeysTable />);
await waitFor(() => {
expect(screen.getByText("Showing 1 - 1 of 1 results")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 1")).toBeInTheDocument();
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-1 of 1");
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 1");
});
});
});
describe("refetch button", () => {
it("should show Fetch button in normal state", () => {
describe("refresh button", () => {
it("renders an enabled refresh control in the normal state", () => {
renderWithProviders(<VirtualKeysTable />);
const fetchButton = screen.getByTitle("Fetch data");
expect(fetchButton).toBeInTheDocument();
expect(fetchButton).not.toBeDisabled();
expect(screen.getByText("Fetch")).toBeInTheDocument();
const refresh = screen.getByTestId("datatable-refresh");
expect(refresh).toBeInTheDocument();
expect(refresh).not.toBeDisabled();
});
it("should show Fetching state and keep table data visible during refetch", () => {
it("disables the refresh control while a fetch is in flight but keeps data visible", () => {
mockUseKeys.mockReturnValue(keysResult([mockKey], {}, { isFetching: true }));
renderWithProviders(<VirtualKeysTable />);
expect(screen.getByText("Fetching")).toBeInTheDocument();
expect(screen.getByTitle("Fetch data")).toBeDisabled();
expect(screen.getByTestId("datatable-refresh")).toBeDisabled();
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
expect(screen.queryByText("🚅 Loading keys...")).not.toBeInTheDocument();
});
it("should call refetch when Fetch button is clicked", () => {
it("calls refetch when the refresh control is clicked", () => {
const mockRefetch = vi.fn();
mockUseKeys.mockReturnValue(keysResult([mockKey], {}, { refetch: mockRefetch }));
renderWithProviders(<VirtualKeysTable />);
fireEvent.click(screen.getByTitle("Fetch data"));
fireEvent.click(screen.getByTestId("datatable-refresh"));
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
it("should show Fetch button enabled on error so user can retry", () => {
mockUseKeys.mockReturnValue(keysResult([], {}, { data: null, isError: true }));
renderWithProviders(<VirtualKeysTable />);
const fetchButton = screen.getByTitle("Fetch data");
expect(fetchButton).not.toBeDisabled();
expect(screen.getByText("Fetch")).toBeInTheDocument();
});
});
describe("Status column reflects key.blocked / scim_blocked metadata", () => {
it("should render Active for a non-blocked key", async () => {
describe("Status column reflects blocked / expiry / scim metadata", () => {
it("renders Active for a non-blocked, unexpired key", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, blocked: false, metadata: {} }]));
renderWithProviders(<VirtualKeysTable />);
@ -459,7 +446,19 @@ describe("Status column reflects key.blocked / scim_blocked metadata", () => {
});
});
it("should render Blocked when key.blocked is true", async () => {
it("renders Expired when the expiry date has passed", async () => {
mockUseKeys.mockReturnValue(
keysResult([{ ...mockKey, blocked: false, metadata: {}, expires: "2020-01-01T00:00:00Z" }]),
);
renderWithProviders(<VirtualKeysTable />);
await waitFor(() => {
expect(screen.getByTestId(`key-status-${mockKey.token_id}`)).toHaveTextContent("Expired");
});
});
it("renders Blocked when key.blocked is true", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, blocked: true, metadata: {} }]));
renderWithProviders(<VirtualKeysTable />);
@ -470,7 +469,7 @@ describe("Status column reflects key.blocked / scim_blocked metadata", () => {
expect(screen.queryByText(/Blocked by SCIM/i)).not.toBeInTheDocument();
});
it("should mark a SCIM-blocked key with the SCIM tooltip reason", async () => {
it("marks a SCIM-blocked key with the SCIM tooltip reason", async () => {
mockUseKeys.mockReturnValue(keysResult([{ ...mockKey, blocked: true, metadata: { scim_blocked: true } }]));
renderWithProviders(<VirtualKeysTable />);

View file

@ -0,0 +1,353 @@
"use client";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ColumnDef } from "@tanstack/react-table";
import { Popover, Typography } from "antd";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { Skeleton } from "@/components/ui/skeleton";
import {
DateCell,
IdCell,
IdentityCell,
ModelsCell,
SpendBudgetCell,
StatusBadge,
type StatusTone,
} from "@/components/shared/table_cells";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
import { Organization } from "../networking";
interface KeyStatus {
tone: StatusTone;
label: string;
tooltip?: string;
}
const getKeyStatus = (key: KeyResponse): KeyStatus => {
if (key.blocked === true) {
const isScimBlocked = (key.metadata as Record<string, unknown> | null | undefined)?.scim_blocked === true;
return {
tone: "error",
label: "Blocked",
tooltip: isScimBlocked
? "Blocked by SCIM (external identity provider deactivated or deleted the owning user)."
: "Blocked. Requests using this key will be rejected with 401.",
};
}
const expiresAt = key.expires ? Date.parse(key.expires) : Number.NaN;
if (!Number.isNaN(expiresAt) && expiresAt < Date.now()) {
return { tone: "warning", label: "Expired", tooltip: "This key has passed its expiry date." };
}
return { tone: "success", label: "Active" };
};
const UserPopoverCell = ({
userAlias,
userEmail,
userId,
width,
}: {
userAlias: string | null;
userEmail: string | null;
userId: string | null;
width: number;
}) => {
const displayValue = userAlias || userEmail || userId;
const isDefaultAdmin = userId === "default_user_id";
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 (
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
<span className="font-mono text-xs truncate block cursor-default" style={{ maxWidth: width, overflow: "hidden" }}>
{displayValue || "-"}
</span>
</Popover>
);
};
const InfoHeader = ({ label, tooltip }: { label: string; tooltip: string }) => (
<span className="flex items-center gap-1">
{label}
<Popover content={tooltip} trigger="hover">
<InfoCircleOutlined className="text-gray-400 text-xs cursor-help" />
</Popover>
</span>
);
interface KeyTableColumnsDeps {
allTeams: Team[];
organizations: Organization[];
onSelectKey: (key: KeyResponse) => void;
}
export const getKeyTableColumns = ({
allTeams,
organizations,
onSelectKey,
}: KeyTableColumnsDeps): ColumnDef<KeyResponse>[] => [
{
id: "key_alias",
accessorKey: "key_alias",
meta: {
title: "Key",
renderSkeleton: () => (
<div className="flex flex-col gap-1 py-1">
<Skeleton className="h-4 w-32" />
<div className="flex items-center gap-2">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
</div>
),
},
header: ({ column }) => <DataTableSortHeader column={column} title="Key" variant="header-cycle" />,
size: 260,
enableSorting: true,
cell: ({ row }) => {
const status = getKeyStatus(row.original);
return (
<IdentityCell
title={row.original.key_alias || "-"}
subtitle={row.original.key_name}
badge={
<StatusBadge
tone={status.tone}
label={status.label}
tooltip={status.tooltip}
dataTestId={`key-status-${row.original.token_id}`}
/>
}
onClick={() => onSelectKey(row.original)}
/>
);
},
},
{
id: "token",
accessorKey: "token",
meta: { title: "Key ID" },
header: ({ column }) => <DataTableSortHeader column={column} title="Key ID" variant="header-cycle" />,
size: 120,
enableSorting: true,
cell: (info) => <IdCell value={info.getValue() as string | null} onClick={() => onSelectKey(info.row.original)} />,
},
{
id: "team_alias",
accessorKey: "team_id",
meta: { title: "Team" },
header: "Team",
size: 120,
enableSorting: false,
cell: (info) => {
const teamId = info.getValue() as string | null;
if (!teamId) return "-";
const team = allTeams.find((t) => t.team_id === teamId);
const displayValue = team?.team_alias || teamId;
const width = info.cell.column.getSize();
return (
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
{displayValue}
</span>
);
},
},
{
id: "organization_alias",
accessorKey: "org_id",
meta: { title: "Organization" },
header: "Organization",
size: 140,
enableSorting: false,
cell: (info) => {
const orgId = info.getValue() as string | null;
if (!orgId) return "-";
const org = organizations.find((o) => o.organization_id === orgId);
const displayValue = org?.organization_alias || orgId;
const width = info.cell.column.getSize();
return (
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
{displayValue}
</span>
);
},
},
{
id: "user",
accessorKey: "user",
meta: { title: "User" },
header: () => (
<InfoHeader label="User" tooltip="Displays the first available value: User Alias, User Email, or User ID." />
),
size: 160,
enableSorting: false,
cell: ({ row }) => {
const key = row.original;
return (
<UserPopoverCell
userAlias={key.user?.user_alias ?? null}
userEmail={key.user?.user_email ?? key.user_email ?? null}
userId={key.user_id ?? null}
width={160}
/>
);
},
},
{
id: "created_at",
accessorKey: "created_at",
meta: { title: "Created At" },
header: ({ column }) => <DataTableSortHeader column={column} title="Created At" variant="header-cycle" />,
size: 120,
enableSorting: true,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" />,
},
{
id: "created_by",
accessorKey: "created_by",
meta: { title: "Created By" },
header: "Created By",
size: 160,
enableSorting: false,
cell: (info) => {
const userId = info.getValue() as string | null;
if (!userId) return "-";
const createdByUser = info.row.original.created_by_user;
return (
<UserPopoverCell
userAlias={createdByUser?.user_alias ?? null}
userEmail={createdByUser?.user_email ?? null}
userId={userId}
width={160}
/>
);
},
},
{
id: "updated_at",
accessorKey: "updated_at",
meta: { title: "Updated At" },
header: ({ column }) => <DataTableSortHeader column={column} title="Updated At" variant="header-cycle" />,
size: 120,
enableSorting: true,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "last_active",
accessorKey: "last_active",
meta: { title: "Last Active" },
header: () => (
<InfoHeader
label="Last Active"
tooltip="This is a new field and is not backfilled. Only new key usage will update this value."
/>
),
size: 130,
enableSorting: false,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Unknown" />,
},
{
id: "expires",
accessorKey: "expires",
meta: { title: "Expires" },
header: "Expires",
size: 120,
enableSorting: false,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "spend",
accessorKey: "spend",
meta: { title: "Spend / Budget", skeleton: "meter" },
header: ({ column }) => <DataTableSortHeader column={column} title="Spend / Budget" variant="header-cycle" />,
size: 180,
enableSorting: true,
cell: ({ row }) => {
const teamId = row.original.team_id;
const team = allTeams.find((t) => t.team_id === teamId);
return (
<SpendBudgetCell
spend={row.original.spend}
maxBudget={row.original.max_budget}
teamMaxBudget={team?.max_budget ?? null}
/>
);
},
},
{
id: "budget_reset_at",
accessorKey: "budget_reset_at",
meta: { title: "Budget Reset" },
header: "Budget Reset",
size: 130,
enableSorting: false,
cell: (info) => <DateCell value={info.getValue() as string | null} fallback="Never" />,
},
{
id: "models",
accessorKey: "models",
meta: { title: "Models", skeleton: "chips" },
header: "Models",
size: 220,
enableSorting: false,
cell: (info) => <ModelsCell models={info.getValue() as string[] | null | undefined} />,
},
{
id: "rate_limits",
meta: { title: "Rate Limits" },
header: "Rate Limits",
size: 140,
enableSorting: false,
cell: ({ row }) => {
const key = row.original;
return (
<div className="text-xs">
<div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div>
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div>
</div>
);
},
},
];
export const KEY_TABLE_HIDDEN_COLUMNS: Record<string, boolean> = {
token: false,
organization_alias: false,
created_by: false,
updated_at: false,
expires: false,
budget_reset_at: false,
rate_limits: false,
};

View file

@ -303,6 +303,38 @@ describe("DataTable loading", () => {
// per-column widths differ instead of every cell sharing one fixed width
expect(new Set(bars.map((bar) => bar.className)).size).toBeGreaterThan(1);
});
it("renders shape-specific skeletons for badge, chips, and meter columns", () => {
const columns: ColumnDef<Person, unknown>[] = [
{ id: "badge", header: "Badge", meta: { skeleton: "badge" }, cell: () => null },
{ id: "chips", header: "Chips", meta: { skeleton: "chips" }, cell: () => null },
{ id: "meter", header: "Meter", meta: { skeleton: "meter" }, cell: () => null },
];
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
const firstRow = screen.getAllByTestId("skeleton-row").at(0);
const cells = Array.from(firstRow?.querySelectorAll("td") ?? []);
const barsIn = (cell: Element | undefined) => cell?.querySelectorAll('[data-slot="skeleton"]').length ?? 0;
// badge = a single pill, chips = three pills, meter = value bar + track bar
expect(barsIn(cells[0])).toBe(1);
expect(cells[0]?.querySelector('[data-slot="skeleton"]')?.className).toContain("rounded-full");
expect(barsIn(cells[1])).toBe(3);
expect(barsIn(cells[2])).toBe(2);
});
it("uses a column's renderSkeleton override when provided", () => {
const columns: ColumnDef<Person, unknown>[] = [
{
id: "custom",
header: "Custom",
meta: { renderSkeleton: () => <div data-testid="custom-skeleton">loading</div> },
cell: () => null,
},
];
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
expect(screen.getAllByTestId("custom-skeleton").length).toBeGreaterThan(0);
});
});
describe("DataTable column visibility", () => {

View file

@ -338,7 +338,11 @@ const SKELETON_WIDTHS = ["w-[58%]", "w-[44%]", "w-[70%]", "w-[50%]", "w-[64%]",
function SkeletonCell<TData>({ column, index }: { column: Column<TData, unknown> | undefined; index: number }) {
const meta = column?.columnDef.meta;
const width = SKELETON_WIDTHS[index % SKELETON_WIDTHS.length];
if (meta?.skeleton === "twoLine") {
const shape = meta?.skeleton;
if (meta?.renderSkeleton !== undefined) {
return <>{meta.renderSkeleton()}</>;
}
if (shape === "twoLine") {
return (
<div className="flex flex-col gap-2">
<Skeleton className={cn("h-3.5", width)} />
@ -346,6 +350,26 @@ function SkeletonCell<TData>({ column, index }: { column: Column<TData, unknown>
</div>
);
}
if (shape === "badge") {
return <Skeleton className={cn("h-5 w-16 rounded-full", meta?.numeric ? "ml-auto" : "")} />;
}
if (shape === "chips") {
return (
<div className="flex items-center gap-1.5">
<Skeleton className="h-5 w-14 rounded-full" />
<Skeleton className="h-5 w-20 rounded-full" />
<Skeleton className="h-5 w-9 rounded-full opacity-65" />
</div>
);
}
if (shape === "meter") {
return (
<div className="flex flex-col gap-1.5">
<Skeleton className="h-3.5 w-24" />
<Skeleton className="h-1.5 w-full rounded-full" />
</div>
);
}
return <Skeleton className={cn("h-3.5", width, meta?.numeric ? "ml-auto" : "")} />;
}

View file

@ -1,4 +1,5 @@
import type { RowData } from "@tanstack/react-table";
import type * as React from "react";
import type { ColumnPinnedSide, DataTableSkeletonShape } from "./types";
@ -10,5 +11,7 @@ declare module "@tanstack/react-table" {
title?: string;
pinned?: ColumnPinnedSide;
skeleton?: DataTableSkeletonShape;
/** Full control over this column's loading skeleton, for cells the built-in shapes can't mirror. */
renderSkeleton?: () => React.ReactNode;
}
}

View file

@ -18,7 +18,7 @@ export type FilterMode = "none" | "client" | "server";
export type ColumnResizeMode = "onEnd" | "onChange";
export type DataTableSize = "compact" | "default";
export type ColumnPinnedSide = "left" | "right";
export type DataTableSkeletonShape = "text" | "twoLine";
export type DataTableSkeletonShape = "text" | "twoLine" | "badge" | "chips" | "meter";
export interface DataTableProps<TData extends RowData, TValue> {
data: TData[];

View file

@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { PageHeader } from "./PageHeader";
describe("PageHeader", () => {
it("renders the title as a heading", () => {
render(<PageHeader title="Virtual Keys" />);
expect(screen.getByRole("heading", { name: "Virtual Keys" })).toBeInTheDocument();
});
it("renders the subtitle, icon, and actions when provided", () => {
render(
<PageHeader
title="Virtual Keys"
subtitle="Every key that authenticates requests"
icon={<svg data-testid="icon" />}
actions={<button>Create New Key</button>}
/>,
);
expect(screen.getByText("Every key that authenticates requests")).toBeInTheDocument();
expect(screen.getByTestId("icon")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create New Key" })).toBeInTheDocument();
});
it("omits the optional slots when not provided", () => {
render(<PageHeader title="Virtual Keys" />);
expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(document.querySelector("p")).toBeNull();
});
});

View file

@ -0,0 +1,29 @@
"use client";
import * as React from "react";
interface PageHeaderProps {
title: React.ReactNode;
subtitle?: React.ReactNode;
icon?: React.ReactNode;
actions?: React.ReactNode;
}
export function PageHeader({ title, subtitle, icon, actions }: PageHeaderProps) {
return (
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex items-center gap-3">
{icon != null && (
<span className="flex size-9 flex-none items-center justify-center rounded-lg bg-primary text-primary-foreground">
{icon}
</span>
)}
<div className="min-w-0">
<h1 className="text-xl font-semibold tracking-tight text-foreground">{title}</h1>
{subtitle != null && <p className="mt-0.5 text-sm text-muted-foreground">{subtitle}</p>}
</div>
</div>
{actions != null && <div className="flex items-center gap-2">{actions}</div>}
</div>
);
}

View file

@ -0,0 +1,64 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { SearchSelect } from "./SearchSelect";
const OPTIONS = [
{ label: "Acme Prod", value: "team-1" },
{ label: "Growth", value: "team-2" },
{ label: "Data Team", value: "team-3" },
];
describe("SearchSelect", () => {
it("renders the placeholder when nothing is selected", () => {
render(<SearchSelect options={OPTIONS} onValueChange={vi.fn()} placeholder="Select Team…" />);
expect(screen.getByPlaceholderText("Select Team…")).toBeInTheDocument();
});
it("shows the selected option's label in the field", () => {
render(<SearchSelect options={OPTIONS} value="team-2" onValueChange={vi.fn()} />);
expect(screen.getByRole("combobox")).toHaveValue("Growth");
});
it("shows a clear control only when a value is selected", () => {
const { rerender } = render(<SearchSelect options={OPTIONS} onValueChange={vi.fn()} />);
expect(document.querySelector('[data-slot="combobox-clear"]')).toBeNull();
rerender(<SearchSelect options={OPTIONS} value="team-1" onValueChange={vi.fn()} />);
expect(document.querySelector('[data-slot="combobox-clear"]')).not.toBeNull();
});
it("filters the options client-side as you type", async () => {
const user = userEvent.setup();
render(<SearchSelect options={OPTIONS} onValueChange={vi.fn()} />);
const input = screen.getByRole("combobox");
await user.click(input);
await user.type(input, "grow");
expect(await screen.findByText("Growth")).toBeInTheDocument();
expect(screen.queryByText("Acme Prod")).not.toBeInTheDocument();
});
it("renders a muted sublabel and matches it when searching", async () => {
const user = userEvent.setup();
render(
<SearchSelect
options={[{ label: "Acme Prod", value: "team-1", sublabel: "team-abc-123" }]}
onValueChange={vi.fn()}
/>,
);
const input = screen.getByRole("combobox");
await user.click(input);
expect(await screen.findByText("team-abc-123")).toBeInTheDocument();
await user.type(input, "abc-123");
expect(await screen.findByText("Acme Prod")).toBeInTheDocument();
});
it("selects an option and reports its value", async () => {
const onValueChange = vi.fn();
const user = userEvent.setup();
render(<SearchSelect options={OPTIONS} onValueChange={onValueChange} />);
await user.click(screen.getByRole("combobox"));
await user.click(await screen.findByText("Growth"));
expect(onValueChange).toHaveBeenCalledWith("team-2");
});
});

View file

@ -0,0 +1,76 @@
"use client";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
export interface SearchSelectOption {
label: string;
value: string;
/** Optional muted second line (e.g. an id); also matched when searching. */
sublabel?: string;
}
interface SearchSelectProps {
options: SearchSelectOption[];
value?: string;
onValueChange: (value: string) => void;
placeholder?: string;
emptyText?: string;
disabled?: boolean;
className?: string;
}
export function SearchSelect({
options,
value,
onValueChange,
placeholder = "Select…",
emptyText = "No results",
disabled = false,
className,
}: SearchSelectProps) {
const selected = options.find((option) => option.value === value) ?? null;
return (
<Combobox
items={options}
value={selected}
onValueChange={(item: SearchSelectOption | null) => onValueChange(item?.value ?? "")}
isItemEqualToValue={(a: SearchSelectOption, b: SearchSelectOption) => a.value === b.value}
itemToStringLabel={(item: SearchSelectOption) => item.label}
filter={(item: SearchSelectOption, query: string) => {
const q = query.trim().toLowerCase();
if (!q) return true;
return item.label.toLowerCase().includes(q) || (item.sublabel?.toLowerCase().includes(q) ?? false);
}}
disabled={disabled}
>
<ComboboxInput
placeholder={placeholder}
showClear={value != null && value !== ""}
className={`w-full ${className ?? ""}`}
/>
<ComboboxContent>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
<ComboboxList>
{(item: SearchSelectOption) => (
<ComboboxItem key={item.value} value={item}>
<span className="flex min-w-0 flex-col">
<span className="truncate">{item.label}</span>
{item.sublabel != null && item.sublabel !== "" && (
<span className="truncate text-xs text-muted-foreground">{item.sublabel}</span>
)}
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}

View file

@ -0,0 +1,38 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { IdentityCell } from "./identity_cell";
describe("IdentityCell", () => {
it("renders the title", () => {
render(<IdentityCell title="prod-gateway" />);
expect(screen.getByText("prod-gateway")).toBeInTheDocument();
});
it("renders the subtitle and an inline badge together", () => {
render(<IdentityCell title="prod-gateway" subtitle="sk-...v0Pw" badge={<span>Active</span>} />);
expect(screen.getByText("sk-...v0Pw")).toBeInTheDocument();
expect(screen.getByText("Active")).toBeInTheDocument();
});
it("omits the subtitle row when there is no subtitle or badge", () => {
render(<IdentityCell title="a" />);
expect(document.querySelector("span.font-mono")).toBeNull();
});
it("renders a static div (no button) when not clickable", () => {
render(<IdentityCell title="a" subtitle="b" />);
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
it("renders a clickable button and fires onClick", async () => {
const onClick = vi.fn();
const user = userEvent.setup();
render(<IdentityCell title="prod-gateway" subtitle="sk-...v0Pw" onClick={onClick} />);
const button = screen.getByRole("button");
expect(button.querySelector(".lucide-chevron-right")).not.toBeNull();
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,48 @@
"use client";
import { ChevronRight } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/cva.config";
interface IdentityCellProps {
title: React.ReactNode;
subtitle?: React.ReactNode;
badge?: React.ReactNode;
onClick?: () => void;
className?: string;
titleClassName?: string;
}
export function IdentityCell({ title, subtitle, badge, onClick, className, titleClassName }: IdentityCellProps) {
const hasSubtitleRow = (subtitle != null && subtitle !== "") || badge != null;
const body = (
<div className="flex min-w-0 flex-col gap-0.5">
<span className={cn("truncate text-sm font-medium text-foreground", titleClassName)}>{title}</span>
{hasSubtitleRow && (
<span className="flex min-w-0 items-center gap-2">
{subtitle != null && subtitle !== "" && (
<span className="truncate font-mono text-xs text-muted-foreground">{subtitle}</span>
)}
{badge}
</span>
)}
</div>
);
if (onClick != null) {
return (
<button
type="button"
onClick={onClick}
className={cn("group flex w-full items-center gap-2 rounded-md py-1 text-left", className)}
>
{body}
<ChevronRight className="ml-auto size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
</button>
);
}
return <div className={cn("min-w-0", className)}>{body}</div>;
}

View file

@ -1,5 +1,8 @@
export { CellTooltip } from "./cell_tooltip";
export { DateCell, formatCellDate, formatFullTimestamp, type DatePrecision } from "./date_cell";
export { IdCell, type IdCellVariant } from "./id_cell";
export { IdentityCell } from "./identity_cell";
export { ModelsCell } from "./models_cell";
export { MoneyCell } from "./money_cell";
export { SpendBudgetCell } from "./spend_budget_cell";
export { StatusBadge, type StatusTone } from "./status_badge";

View file

@ -0,0 +1,45 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { ModelsCell } from "./models_cell";
describe("ModelsCell", () => {
it("shows 'All Proxy Models' when the list is empty, null, or undefined", () => {
const { rerender } = render(<ModelsCell models={[]} />);
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
rerender(<ModelsCell models={null} />);
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
rerender(<ModelsCell models={undefined} />);
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
it("renders every model with no overflow badge when at or below the limit", () => {
render(<ModelsCell models={["gpt-4o", "claude-sonnet-4-5", "o3-mini"]} maxVisible={3} />);
expect(screen.getByText("gpt-4o")).toBeInTheDocument();
expect(screen.getByText("claude-sonnet-4-5")).toBeInTheDocument();
expect(screen.getByText("o3-mini")).toBeInTheDocument();
expect(screen.queryByText(/more$/)).not.toBeInTheDocument();
});
it("collapses models beyond the limit into a '+N more' badge", () => {
render(<ModelsCell models={["a", "b", "c", "d", "e"]} maxVisible={2} />);
expect(screen.getByText("a")).toBeInTheDocument();
expect(screen.getByText("b")).toBeInTheDocument();
expect(screen.queryByText("c")).not.toBeInTheDocument();
expect(screen.getByText("+3 more")).toBeInTheDocument();
});
it("reveals the hidden models in a tooltip on hover", async () => {
const user = userEvent.setup();
render(<ModelsCell models={["a", "b", "c", "d"]} maxVisible={2} />);
await user.hover(screen.getByText("+2 more"));
expect(await screen.findByText("c")).toBeInTheDocument();
expect(await screen.findByText("d")).toBeInTheDocument();
});
it("labels the all-proxy-models wildcard", () => {
render(<ModelsCell models={["all-proxy-models"]} />);
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,56 @@
"use client";
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
import { Badge } from "@/components/ui/badge";
import { CellTooltip } from "./cell_tooltip";
interface ModelsCellProps {
models: string[] | null | undefined;
maxVisible?: number;
}
const WILDCARD_MODEL = "all-proxy-models";
const formatModel = (model: string): string => {
if (model === WILDCARD_MODEL) {
return "All Proxy Models";
}
const name = getModelDisplayName(model);
return name.length > 30 ? `${name.slice(0, 30)}...` : name;
};
export function ModelsCell({ models, maxVisible = 3 }: ModelsCellProps) {
if (!Array.isArray(models) || models.length === 0) {
return <Badge variant="secondary">All Proxy Models</Badge>;
}
const visible = models.slice(0, maxVisible);
const overflow = models.slice(maxVisible);
return (
<div className="flex flex-wrap items-center gap-1">
{visible.map((model, index) => (
<Badge key={index} variant={model === WILDCARD_MODEL ? "secondary" : "outline"}>
{formatModel(model)}
</Badge>
))}
{overflow.length > 0 && (
<CellTooltip
content={
<div className="flex max-w-[280px] flex-col gap-0.5">
{overflow.map((model, index) => (
<span key={index}>{formatModel(model)}</span>
))}
</div>
}
trigger={
<Badge variant="outline" className="cursor-default">
+{overflow.length} more
</Badge>
}
/>
)}
</div>
);
}

View file

@ -0,0 +1,53 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SpendBudgetCell } from "./spend_budget_cell";
const indicator = (container: HTMLElement) => container.querySelector('[data-slot="meter-indicator"]');
describe("SpendBudgetCell", () => {
it("shows Unlimited and renders no meter when there is no budget", () => {
const { container } = render(<SpendBudgetCell spend={0.5} maxBudget={null} />);
expect(screen.getByText("· Unlimited")).toBeInTheDocument();
expect(screen.queryByRole("meter")).not.toBeInTheDocument();
expect(indicator(container)).toBeNull();
});
it("shows $0.00 for zero or undefined spend, never a hyphen", () => {
const { rerender } = render(<SpendBudgetCell spend={0} maxBudget={100} />);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.queryByText("-")).not.toBeInTheDocument();
rerender(<SpendBudgetCell spend={null} maxBudget={null} />);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.queryByText("-")).not.toBeInTheDocument();
});
it("renders a meter carrying the spend and budget when a budget exists", () => {
render(<SpendBudgetCell spend={25} maxBudget={100} />);
const meter = screen.getByRole("meter");
expect(meter).toHaveAttribute("aria-valuenow", "25");
expect(meter).toHaveAttribute("aria-valuemax", "100");
expect(screen.getByText("of $100")).toBeInTheDocument();
});
it("keeps the default tone below 80% usage", () => {
const { container } = render(<SpendBudgetCell spend={50} maxBudget={100} />);
expect(indicator(container)?.className).toContain("bg-primary");
});
it("switches to the warning tone at 80% usage", () => {
const { container } = render(<SpendBudgetCell spend={80} maxBudget={100} />);
expect(indicator(container)?.className).toContain("bg-amber-500");
});
it("switches to the over tone above 100% usage", () => {
const { container } = render(<SpendBudgetCell spend={150} maxBudget={100} />);
expect(indicator(container)?.className).toContain("bg-destructive");
});
it("falls back to the team budget and labels it", () => {
render(<SpendBudgetCell spend={10} maxBudget={null} teamMaxBudget={200} />);
expect(screen.getByText("of $200 (Team)")).toBeInTheDocument();
expect(screen.getByRole("meter")).toHaveAttribute("aria-valuemax", "200");
});
});

View file

@ -0,0 +1,44 @@
"use client";
import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter";
import { formatNumberWithCommas, getSpendString } from "@/utils/dataUtils";
interface SpendBudgetCellProps {
spend: number | null | undefined;
maxBudget: number | null | undefined;
teamMaxBudget?: number | null;
}
const meterTone = (pct: number): "default" | "warning" | "over" => {
if (pct > 100) return "over";
if (pct >= 80) return "warning";
return "default";
};
export function SpendBudgetCell({ spend, maxBudget, teamMaxBudget }: SpendBudgetCellProps) {
const spendValue = typeof spend === "number" && !Number.isNaN(spend) ? spend : 0;
const budget = maxBudget ?? teamMaxBudget ?? null;
const isTeamBudget = maxBudget == null && teamMaxBudget != null;
const hasBudget = typeof budget === "number" && budget > 0;
const pct = hasBudget ? (spendValue / budget) * 100 : 0;
const spendText = spendValue > 0 ? getSpendString(spendValue, 4) : "$0.00";
const budgetLabel =
budget === null ? "· Unlimited" : `of $${formatNumberWithCommas(budget)}${isTeamBudget ? " (Team)" : ""}`;
return (
<div className="flex min-w-[130px] flex-col gap-1">
<div className="whitespace-nowrap text-xs">
<span className="font-medium tabular-nums text-foreground">{spendText}</span>{" "}
<span className="text-muted-foreground">{budgetLabel}</span>
</div>
{hasBudget && (
<Meter value={spendValue} max={budget} aria-valuetext={`${spendText} of $${formatNumberWithCommas(budget)}`}>
<MeterTrack>
<MeterIndicator tone={meterTone(pct)} />
</MeterTrack>
</Meter>
)}
</div>
);
}

View file

@ -0,0 +1,266 @@
"use client";
import * as React from "react";
import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { cn } from "@/lib/cva.config";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react";
const Combobox = ComboboxPrimitive.Root;
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}
const ComboboxTrigger = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Trigger>,
ComboboxPrimitive.Trigger.Props
>(({ className, children, ...props }, ref) => {
return (
<ComboboxPrimitive.Trigger
ref={ref}
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
);
});
ComboboxTrigger.displayName = "ComboboxTrigger";
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
);
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean;
showClear?: boolean;
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input render={<InputGroupInput disabled={disabled} />} {...props} />
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
render={<ComboboxTrigger />}
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<ComboboxPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "anchor">) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
className,
)}
{...props}
/>
);
}
function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />}
>
<CheckIcon className="pointer-events-none" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return <ComboboxPrimitive.Group data-slot="combobox-group" className={cn(className)} {...props} />;
}
function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
);
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
className,
)}
{...props}
/>
);
}
function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean;
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
);
}
function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
);
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
};

View file

@ -0,0 +1,140 @@
"use client";
import * as React from "react";
import { type VariantProps } from "cva";
import { cn, cva } from "@/lib/cva.config";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva({
base: "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
variants: {
align: {
"inline-start": "order-first pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem]",
"inline-end": "order-last pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end": "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
});
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva({
base: "flex items-center gap-2 text-sm shadow-none",
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
});
const InputGroupButton = React.forwardRef<
React.ComponentRef<typeof Button>,
Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset";
}
>(({ className, type = "button", variant = "ghost", size = "xs", ...props }, ref) => {
return (
<Button
ref={ref}
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
});
InputGroupButton.displayName = "InputGroupButton";
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
const InputGroupInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<"input">>(
({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
},
);
InputGroupInput.displayName = "InputGroupInput";
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea };

View file

@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/cva.config";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}
export { Textarea };

View file

@ -288,18 +288,21 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
<div className="w-full mx-4 h-[75vh]">
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
<Col numColSpan={1} className="flex flex-col gap-2">
{canCreateKey && (
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
team={selectedTeam as Team | null}
teams={teams as Team[]}
data={keys}
addKey={addKey}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
)}
<VirtualKeysTable />
<VirtualKeysTable
headerActions={
canCreateKey ? (
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
team={selectedTeam as Team | null}
teams={teams as Team[]}
data={keys}
addKey={addKey}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
) : undefined
}
/>
</Col>
</Grid>
</div>