feat(ui): link the User ID, Created By and Deleted By cells on Deleted Keys (#40750)

* feat(ui): link the User ID, Created By and Deleted By cells on Deleted Keys

All three columns rendered as plain text, so auditing a deleted key meant
copying an id into the Users page search box. Route them through
IdentityCell with userDetailHref, which keeps the proxy admin placeholder
unlinked. User Email and Team Alias stay as they are: the deleted key table
has no column for either, so the API never populates them.

Claude-Session: https://claude.ai/code/session_01NfwfQhamRNnSqgXMUjf3h4

* test(ui): mount a router mock for the Deleted Keys page test

The page test renders the table, and the newly linked cells call useRouter,
which throws without an App Router mounted. Matches how the other 35 test
files in the suite stub next/navigation.

Claude-Session: https://claude.ai/code/session_01NfwfQhamRNnSqgXMUjf3h4
This commit is contained in:
ryan-crabbe-berri 2026-09-11 17:48:57 -07:00 committed by GitHub
parent 1be930664f
commit 06964e5603
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 36 additions and 4 deletions

View file

@ -5,6 +5,8 @@ import { renderWithProviders } from "../../../tests/test-utils";
import DeletedKeysPage from "./DeletedKeysPage";
import { useDeletedKeys, DeletedKeyResponse } from "@/app/(dashboard)/hooks/keys/useKeys";
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({
useDeletedKeys: vi.fn(),
}));

View file

@ -5,6 +5,8 @@ import { renderWithProviders } from "../../../../tests/test-utils";
import { DeletedKeysTable } from "./DeletedKeysTable";
import { DeletedKeyResponse } from "@/app/(dashboard)/hooks/keys/useKeys";
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
const makeDeletedKey = (overrides: Partial<DeletedKeyResponse> = {}): DeletedKeyResponse =>
({
token: "sk-1234567890abcdef",
@ -86,3 +88,19 @@ it("should show the empty state when there are no deleted keys", () => {
expect(screen.getByText("No deleted keys found")).toBeInTheDocument();
});
it("links the owner, creator and deleter cells to their user detail pages", () => {
renderWithProviders(<DeletedKeysTable {...defaultProps} keys={[makeDeletedKey({ deleted_by: "deleter-1" })]} />);
expect(screen.getByRole("link", { name: "user-1" })).toHaveAttribute("href", "/ui/users?user=user-1");
expect(screen.getByRole("link", { name: "creator-1" })).toHaveAttribute("href", "/ui/users?user=creator-1");
expect(screen.getByRole("link", { name: "deleter-1" })).toHaveAttribute("href", "/ui/users?user=deleter-1");
});
it("leaves the default_user_id placeholder unlinked", () => {
const placeholderKey = makeDeletedKey({ user_id: "default_user_id", created_by: "default_user_id" });
renderWithProviders(<DeletedKeysTable {...defaultProps} keys={[placeholderKey]} />);
expect(screen.getAllByText("default_user_id")).toHaveLength(2);
expect(screen.queryByRole("link", { name: "default_user_id" })).not.toBeInTheDocument();
});

View file

@ -3,8 +3,9 @@
import { ColumnDef } from "@tanstack/react-table";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
import { DateCell, IdCell, IdentityCell, MoneyCell } from "@/components/shared/table_cells";
import { DeletedKeyResponse } from "@/app/(dashboard)/hooks/keys/useKeys";
import { userDetailHref } from "@/utils/entityLinks";
function TruncatedTextCell({ value }: { value: string | null | undefined }) {
if (!value) {
@ -17,6 +18,17 @@ function TruncatedTextCell({ value }: { value: string | null | undefined }) {
);
}
function UserLinkCell({ userId }: { userId: string | null | undefined }) {
if (!userId) {
return <span className="text-muted-foreground">-</span>;
}
return (
<span className="block max-w-60" title={userId}>
<IdentityCell title={userId} titleClassName="font-normal" href={userDetailHref(userId)} />
</span>
);
}
export const getDeletedKeysTableColumns = (): ColumnDef<DeletedKeyResponse>[] => [
{
id: "token",
@ -89,7 +101,7 @@ export const getDeletedKeysTableColumns = (): ColumnDef<DeletedKeyResponse>[] =>
header: "User ID",
size: 120,
enableSorting: false,
cell: ({ row }) => <IdCell value={row.original.user_id} variant="plain" />,
cell: ({ row }) => <UserLinkCell userId={row.original.user_id} />,
},
{
id: "created_at",
@ -107,7 +119,7 @@ export const getDeletedKeysTableColumns = (): ColumnDef<DeletedKeyResponse>[] =>
header: "Created By",
size: 120,
enableSorting: false,
cell: ({ row }) => <TruncatedTextCell value={row.original.created_by} />,
cell: ({ row }) => <UserLinkCell userId={row.original.created_by} />,
},
{
id: "deleted_at",
@ -125,6 +137,6 @@ export const getDeletedKeysTableColumns = (): ColumnDef<DeletedKeyResponse>[] =>
header: "Deleted By",
size: 120,
enableSorting: false,
cell: ({ row }) => <TruncatedTextCell value={row.original.deleted_by} />,
cell: ({ row }) => <UserLinkCell userId={row.original.deleted_by} />,
},
];