refactor(ui): migrate policy attachments table onto shared DataTable (#33827)

* refactor(ui): migrate policy attachments table onto shared DataTable

* refactor(ui): pass a specific success message to the attachment copy action
This commit is contained in:
yuneng-jiang 2026-07-18 11:37:39 -07:00 • committed by GitHub
parent e18966625d
commit 377d54e694
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 309 additions and 385 deletions

View file

@ -881,19 +881,6 @@
"count": 1
}
},
"src/app/(dashboard)/policies/_components/attachment_table.test.tsx": {
"react/display-name": {
"count": 1
}
},
"src/app/(dashboard)/policies/_components/attachment_table.tsx": {
"no-nested-ternary": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/policies/_components/guardrail_selection_modal.tsx": {
"no-nested-ternary": {
"count": 1

View file

@ -1,63 +1,17 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "@/../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AttachmentTable from "./attachment_table";
import AttachmentTable from "./AttachmentTable";
import { PolicyAttachment } from "@/components/policies/types";
vi.mock("./impact_popover", () => ({
default: () => <button aria-label="View blast radius" />,
}));
vi.mock("@heroicons/react/outline", () => ({
TrashIcon: function TrashIcon() {
return null;
},
SwitchVerticalIcon: function SwitchVerticalIcon() {
return null;
},
ChevronUpIcon: function ChevronUpIcon() {
return null;
},
ChevronDownIcon: function ChevronDownIcon() {
return null;
default: function ImpactPopoverMock() {
return <button aria-label="View blast radius" />;
},
}));
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
...actual,
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
React.createElement("button", { ...props, ref }, children),
),
Tooltip: ({ children }: { children?: React.ReactNode }) => React.createElement(React.Fragment, null, children),
Switch: ({
checked,
onChange,
className,
}: {
checked?: boolean;
onChange?: (v: boolean) => void;
className?: string;
}) =>
React.createElement("input", {
type: "checkbox",
role: "switch",
checked,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onChange?.(e.target.checked),
className,
}),
Icon: ({ icon: IconComp, onClick, className }: any) =>
React.createElement(
"button",
{ type: "button", onClick, className },
IconComp?.displayName ?? IconComp?.name ?? "icon",
),
};
});
const makeAttachment = (overrides: Partial<PolicyAttachment> = {}): PolicyAttachment => ({
attachment_id: "att-abcdef1",
policy_name: "my-policy",
@ -82,19 +36,26 @@ describe("AttachmentTable", () => {
vi.clearAllMocks();
});
it("should render", () => {
it("should render column headers", () => {
renderWithProviders(<AttachmentTable {...defaultProps} />);
expect(screen.getByText("Attachment ID")).toBeInTheDocument();
expect(screen.getByText("Policy")).toBeInTheDocument();
expect(screen.getByText("Scope")).toBeInTheDocument();
expect(screen.getByText("Teams")).toBeInTheDocument();
expect(screen.getByText("Keys")).toBeInTheDocument();
expect(screen.getByText("Models")).toBeInTheDocument();
expect(screen.getByText("Tags")).toBeInTheDocument();
expect(screen.getByText("Created At")).toBeInTheDocument();
});
it("should show a loading message when isLoading is true", () => {
it("should show skeleton rows when isLoading is true", () => {
renderWithProviders(<AttachmentTable {...defaultProps} isLoading />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(screen.getAllByTestId("skeleton-row").length).toBeGreaterThan(0);
});
it("should show 'No attachments found' when there are no attachments", () => {
it("should show the empty state when there are no attachments", () => {
renderWithProviders(<AttachmentTable {...defaultProps} />);
expect(screen.getByText(/no attachments found/i)).toBeInTheDocument();
expect(screen.getByText("No attachments found")).toBeInTheDocument();
});
it("should render a row for each attachment", () => {
@ -107,13 +68,24 @@ describe("AttachmentTable", () => {
expect(screen.getByText("policy-beta")).toBeInTheDocument();
});
it("should sort rows by created_at descending by default", () => {
const attachments = [
makeAttachment({ attachment_id: "att-old0001", policy_name: "older-policy", created_at: "2024-01-01T00:00:00Z" }),
makeAttachment({ attachment_id: "att-new0001", policy_name: "newer-policy", created_at: "2025-06-01T00:00:00Z" }),
];
renderWithProviders(<AttachmentTable {...defaultProps} attachments={attachments} />);
const rows = screen.getAllByRole("row").slice(1);
expect(within(rows[0]).getByText("newer-policy")).toBeInTheDocument();
expect(within(rows[1]).getByText("older-policy")).toBeInTheDocument();
});
it("should show 'Global (*)' badge when scope is '*'", () => {
const attachments = [makeAttachment({ scope: "*" })];
renderWithProviders(<AttachmentTable {...defaultProps} attachments={attachments} />);
expect(screen.getByText("Global (*)")).toBeInTheDocument();
});
it("should show team tags when the attachment has teams", () => {
it("should show team chips when the attachment has teams", () => {
const attachments = [makeAttachment({ teams: ["team-alpha", "team-beta"] })];
renderWithProviders(<AttachmentTable {...defaultProps} attachments={attachments} />);
expect(screen.getByText("team-alpha")).toBeInTheDocument();
@ -126,18 +98,37 @@ describe("AttachmentTable", () => {
expect(screen.getByText("+2")).toBeInTheDocument();
});
it("should call onDeleteClick with the attachment_id when the delete icon is clicked", async () => {
it("should call onDeleteClick with the attachment_id from the actions menu", async () => {
const attachment = makeAttachment({ attachment_id: "att-del-me1" });
const user = userEvent.setup();
renderWithProviders(<AttachmentTable {...defaultProps} attachments={[attachment]} />);
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByTestId("attachment-actions-att-del-me1"));
await user.click(await screen.findByTestId("attachment-action-delete"));
expect(defaultProps.onDeleteClick).toHaveBeenCalledWith("att-del-me1");
});
it("should not show the delete icon for non-admins", () => {
it("should not show the delete item for non-admins", async () => {
const attachment = makeAttachment({ attachment_id: "att-nonadmin" });
const user = userEvent.setup();
renderWithProviders(<AttachmentTable {...defaultProps} attachments={[attachment]} isAdmin={false} />);
await user.click(screen.getByTestId("attachment-actions-att-nonadmin"));
expect(await screen.findByTestId("attachment-action-copy-id")).toBeInTheDocument();
expect(screen.queryByTestId("attachment-action-delete")).not.toBeInTheDocument();
});
it("should copy the attachment id from the actions menu", async () => {
const attachment = makeAttachment({ attachment_id: "att-copy-me1" });
const user = userEvent.setup();
renderWithProviders(<AttachmentTable {...defaultProps} attachments={[attachment]} />);
await user.click(screen.getByTestId("attachment-actions-att-copy-me1"));
await user.click(await screen.findByTestId("attachment-action-copy-id"));
expect(await window.navigator.clipboard.readText()).toBe("att-copy-me1");
});
it("should show the blast radius action for non-admins", () => {
const attachment = makeAttachment();
renderWithProviders(<AttachmentTable {...defaultProps} attachments={[attachment]} isAdmin={false} />);
expect(screen.queryByRole("button", { name: /TrashIcon/i })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "View blast radius" })).toBeInTheDocument();
});
it("should show the attachment ID as truncated plain mono text", () => {
@ -149,7 +140,7 @@ describe("AttachmentTable", () => {
expect(idElement.className).not.toContain("bg-blue-50");
});
it("should render model tags when the attachment has models", () => {
it("should render model chips when the attachment has models", () => {
const attachments = [makeAttachment({ models: ["gpt-4", "claude-3"] })];
renderWithProviders(<AttachmentTable {...defaultProps} attachments={attachments} />);
expect(screen.getByText("gpt-4")).toBeInTheDocument();

View file

@ -0,0 +1,66 @@
"use client";
import { SortingState } from "@tanstack/react-table";
import { Inbox } from "lucide-react";
import React, { useMemo, useState } from "react";
import { DataTable } from "@/components/shared/DataTable";
import { PolicyAttachment } from "@/components/policies/types";
import { getAttachmentTableColumns } from "./AttachmentTableColumns";
interface AttachmentTableProps {
attachments: PolicyAttachment[];
isLoading: boolean;
onDeleteClick: (attachmentId: string) => void;
isAdmin: boolean;
accessToken: string | null;
}
const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }];
function EmptyState() {
return (
<div className="flex flex-col items-center gap-1 py-6">
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
<Inbox className="size-5 text-muted-foreground" />
</div>
<div className="text-sm font-medium text-foreground">No attachments found</div>
<div className="text-sm text-muted-foreground">
Attach a policy to teams, keys, models, or tags to control where it applies.
</div>
</div>
);
}
const AttachmentTable: React.FC<AttachmentTableProps> = ({
attachments,
isLoading,
onDeleteClick,
isAdmin,
accessToken,
}) => {
const [sorting, setSorting] = useState<SortingState>(DEFAULT_SORTING);
const columns = useMemo(() => {
const deps = { isAdmin, accessToken, onDeleteClick };
return getAttachmentTableColumns(deps);
}, [isAdmin, accessToken, onDeleteClick]);
return (
<DataTable
data={attachments}
columns={columns}
getRowId={(row) => row.attachment_id}
sortingMode="client"
sorting={sorting}
onSortingChange={setSorting}
isLoading={isLoading}
loadingMessage="Loading attachments…"
noDataMessage={<EmptyState />}
size="compact"
/>
);
};
export default AttachmentTable;

View file

@ -0,0 +1,186 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { Copy, MoreHorizontal, Trash2 } from "lucide-react";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { DateCell, IdCell, StatusBadge } from "@/components/shared/table_cells";
import { PolicyAttachment } from "@/components/policies/types";
import { buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/cva.config";
import { copyToClipboard } from "@/utils/dataUtils";
import ImpactPopover from "./impact_popover";
function ChipList({ values }: { values: string[] }) {
if (values.length === 0) {
return <span className="text-muted-foreground">-</span>;
}
return (
<div className="flex flex-wrap items-center gap-1">
{values.slice(0, 2).map((value) => (
<StatusBadge key={value} tone="neutral" label={value} />
))}
{values.length > 2 && (
<StatusBadge tone="neutral" label={`+${values.length - 2}`} tooltip={values.slice(2).join(", ")} />
)}
</div>
);
}
interface AttachmentRowActionsProps {
attachment: PolicyAttachment;
isAdmin: boolean;
onDeleteClick: (attachmentId: string) => void;
}
function AttachmentRowActions({ attachment, isAdmin, onDeleteClick }: AttachmentRowActionsProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger
aria-label="Open attachment actions"
data-testid={`attachment-actions-${attachment.attachment_id}`}
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-muted-foreground")}
>
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem
data-testid="attachment-action-copy-id"
onClick={() => void copyToClipboard(attachment.attachment_id, "Attachment ID copied")}
>
<Copy />
Copy attachment ID
</DropdownMenuItem>
{isAdmin && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
data-testid="attachment-action-delete"
onClick={() => onDeleteClick(attachment.attachment_id)}
>
<Trash2 />
Delete attachment
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
interface AttachmentTableColumnsDeps {
isAdmin: boolean;
accessToken: string | null;
onDeleteClick: (attachmentId: string) => void;
}
export const getAttachmentTableColumns = ({
isAdmin,
accessToken,
onDeleteClick,
}: AttachmentTableColumnsDeps): ColumnDef<PolicyAttachment>[] => [
{
id: "attachment_id",
accessorKey: "attachment_id",
meta: { title: "Attachment ID" },
header: "Attachment ID",
size: 160,
enableSorting: false,
cell: ({ row }) => <IdCell value={row.original.attachment_id} variant="plain" />,
},
{
id: "policy_name",
accessorKey: "policy_name",
meta: { title: "Policy", skeleton: "badge" },
header: ({ column }) => <DataTableSortHeader column={column} title="Policy" />,
size: 180,
enableSorting: true,
cell: ({ row }) => <StatusBadge tone="info" label={row.original.policy_name} />,
},
{
id: "scope",
accessorFn: (row) => row.scope ?? "",
meta: { title: "Scope", skeleton: "badge" },
header: "Scope",
size: 120,
enableSorting: false,
cell: ({ row }) => {
const scope = row.original.scope;
if (!scope) {
return <span className="text-muted-foreground">-</span>;
}
if (scope === "*") {
return <StatusBadge tone="warning" label="Global (*)" />;
}
return (
<span className="block max-w-40 truncate text-xs" title={scope}>
{scope}
</span>
);
},
},
{
id: "teams",
meta: { title: "Teams", skeleton: "chips" },
header: "Teams",
size: 160,
enableSorting: false,
cell: ({ row }) => <ChipList values={row.original.teams ?? []} />,
},
{
id: "keys",
meta: { title: "Keys", skeleton: "chips" },
header: "Keys",
size: 160,
enableSorting: false,
cell: ({ row }) => <ChipList values={row.original.keys ?? []} />,
},
{
id: "models",
meta: { title: "Models", skeleton: "chips" },
header: "Models",
size: 160,
enableSorting: false,
cell: ({ row }) => <ChipList values={row.original.models ?? []} />,
},
{
id: "tags",
meta: { title: "Tags", skeleton: "chips" },
header: "Tags",
size: 160,
enableSorting: false,
cell: ({ row }) => <ChipList values={row.original.tags ?? []} />,
},
{
id: "created_at",
accessorFn: (row) => row.created_at ?? "",
meta: { title: "Created At" },
header: ({ column }) => <DataTableSortHeader column={column} title="Created At" />,
size: 150,
enableSorting: true,
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
{
id: "actions",
meta: { className: "text-right", headerClassName: "text-right" },
header: () => <span className="sr-only">Actions</span>,
size: 88,
enableSorting: false,
enableHiding: false,
cell: ({ row }) => (
<div className="flex items-center justify-end gap-1">
<ImpactPopover attachment={row.original} accessToken={accessToken} />
<AttachmentRowActions attachment={row.original} isAdmin={isAdmin} onDeleteClick={onDeleteClick} />
</div>
),
},
];

View file

@ -1,291 +0,0 @@
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Badge } from "@tremor/react";
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip, Tag } from "antd";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { PolicyAttachment } from "@/components/policies/types";
import ImpactPopover from "./impact_popover";
interface AttachmentTableProps {
attachments: PolicyAttachment[];
isLoading: boolean;
onDeleteClick: (attachmentId: string) => void;
isAdmin: boolean;
accessToken: string | null;
}
const AttachmentTable: React.FC<AttachmentTableProps> = ({
attachments,
isLoading,
onDeleteClick,
isAdmin,
accessToken,
}) => {
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const columns: ColumnDef<PolicyAttachment>[] = [
{
header: "Attachment ID",
accessorKey: "attachment_id",
cell: (info: any) => <IdCell value={info.getValue()} variant="plain" />,
},
{
header: "Policy",
accessorKey: "policy_name",
cell: ({ row }) => {
const attachment = row.original;
return (
<Badge color="blue" size="xs">
{attachment.policy_name}
</Badge>
);
},
},
{
header: "Scope",
accessorKey: "scope",
cell: ({ row }) => {
const attachment = row.original;
if (attachment.scope === "*") {
return (
<Badge color="amber" size="xs">
Global (*)
</Badge>
);
}
return attachment.scope ? (
<span className="text-xs">{attachment.scope}</span>
) : (
<span className="text-xs text-gray-400">-</span>
);
},
},
{
header: "Teams",
accessorKey: "teams",
cell: ({ row }) => {
const attachment = row.original;
const teams = attachment.teams || [];
if (teams.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{teams.slice(0, 2).map((t, i) => (
<Tag key={i} color="cyan" className="text-xs">
{t}
</Tag>
))}
{teams.length > 2 && (
<Tooltip title={teams.slice(2).join(", ")}>
<Tag className="text-xs">+{teams.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
header: "Keys",
accessorKey: "keys",
cell: ({ row }) => {
const attachment = row.original;
const keys = attachment.keys || [];
if (keys.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{keys.slice(0, 2).map((k, i) => (
<Tag key={i} color="purple" className="text-xs">
{k}
</Tag>
))}
{keys.length > 2 && (
<Tooltip title={keys.slice(2).join(", ")}>
<Tag className="text-xs">+{keys.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
header: "Models",
accessorKey: "models",
cell: ({ row }) => {
const attachment = row.original;
const models = attachment.models || [];
if (models.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{models.slice(0, 2).map((m, i) => (
<Tag key={i} color="green" className="text-xs">
{m}
</Tag>
))}
{models.length > 2 && (
<Tooltip title={models.slice(2).join(", ")}>
<Tag className="text-xs">+{models.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
header: "Tags",
accessorKey: "tags",
cell: ({ row }) => {
const attachment = row.original;
const tags = attachment.tags || [];
if (tags.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{tags.slice(0, 2).map((t, i) => (
<Tag key={i} color="orange" className="text-xs">
{t}
</Tag>
))}
{tags.length > 2 && (
<Tooltip title={tags.slice(2).join(", ")}>
<Tag className="text-xs">+{tags.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const attachment = row.original;
return (
<div className="flex space-x-2">
<ImpactPopover attachment={attachment} accessToken={accessToken} />
{isAdmin && (
<Tooltip title="Delete attachment">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onDeleteClick(attachment.attachment_id)}
className="cursor-pointer hover:text-red-500"
/>
</Tooltip>
)}
</div>
);
},
},
];
const table = useReactTable({
data: attachments,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
return (
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
}`}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== "actions" && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
</div>
</TableCell>
</TableRow>
) : attachments.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No attachments found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
};
export default AttachmentTable;

View file

@ -56,21 +56,6 @@ vi.mock("./impact_popover", () => ({
default: () => <button type="button" aria-label="View blast radius" />,
}));
vi.mock("@heroicons/react/outline", () => ({
TrashIcon: function TrashIcon() {
return null;
},
SwitchVerticalIcon: function SwitchVerticalIcon() {
return null;
},
ChevronUpIcon: function ChevronUpIcon() {
return null;
},
ChevronDownIcon: function ChevronDownIcon() {
return null;
},
}));
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
@ -95,8 +80,6 @@ vi.mock("@tremor/react", async (importOriginal) => {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onChange?.(e.target.checked),
className,
}),
Icon: ({ icon: _IconComp, onClick, className }: any) =>
React.createElement("button", { type: "button", onClick, className }, "TrashIcon"),
};
});
@ -163,7 +146,8 @@ describe("PoliciesPanel attachment delete", () => {
expect(screen.getByText("test-policy")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByTestId(`attachment-actions-${EXPECTED_ATTACHMENT_ID}`));
await user.click(await screen.findByTestId("attachment-action-delete"));
const dialog = await screen.findByRole("dialog", {}, { timeout: 5000 });
expect(within(dialog).getByText(/Are you sure you want to delete this attachment/i)).toBeInTheDocument();
@ -195,7 +179,8 @@ describe("PoliciesPanel attachment delete", () => {
expect(screen.getByText("test-policy")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
await user.click(screen.getByTestId(`attachment-actions-${EXPECTED_ATTACHMENT_ID}`));
await user.click(await screen.findByTestId("attachment-action-delete"));
const dialog = await screen.findByRole("dialog", {}, { timeout: 5000 });
const deleteButton = within(dialog).getByRole("button", { name: /^delete$/i });

View file

@ -9,7 +9,7 @@ import PolicyTable from "./PolicyTable";
import PolicyInfoView from "./policy_info";
import AddPolicyForm from "./add_policy_form";
import { FlowBuilderPage } from "./pipeline_flow_builder";
import AttachmentTable from "./attachment_table";
import AttachmentTable from "./AttachmentTable";
import AddAttachmentForm from "./add_attachment_form";
import PolicyTestPanel from "./policy_test_panel";
import PolicyTemplates from "./policy_templates";