Skip to content

Commit 63e10c9

Browse files
refactor(ui): attack paths restyling and component migrations (#10310)
1 parent 97a91bf commit 63e10c9

File tree

22 files changed

+1386
-1177
lines changed

22 files changed

+1386
-1177
lines changed

ui/app/(prowler)/attack-paths/(workflow)/_components/workflow-attack-paths.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const WorkflowAttackPaths = () => {
1212
const pathname = usePathname();
1313

1414
// Determine current step based on pathname
15-
const isQueryBuilderStep = pathname.includes("query-builder");
15+
const isQueryBuilderStep = pathname.includes("/attack-paths");
1616

1717
const currentStep = isQueryBuilderStep ? 1 : 0; // 0-indexed
1818

ui/app/(prowler)/attack-paths/(workflow)/layout.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { GraphLoading } from "./graph-loading";
5+
6+
describe("GraphLoading", () => {
7+
it("uses the provider wizard loading pattern", () => {
8+
render(<GraphLoading />);
9+
10+
expect(screen.getByTestId("graph-loading")).toHaveClass(
11+
"flex",
12+
"min-h-[320px]",
13+
"items-center",
14+
"justify-center",
15+
"gap-4",
16+
"text-center",
17+
);
18+
expect(screen.getByLabelText("Loading")).toHaveClass(
19+
"size-6",
20+
"animate-spin",
21+
);
22+
expect(screen.getByText("Loading Attack Paths graph...")).toHaveClass(
23+
"text-muted-foreground",
24+
"text-sm",
25+
);
26+
});
27+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { FormProvider, useForm } from "react-hook-form";
3+
import { describe, expect, it } from "vitest";
4+
5+
import type { AttackPathQuery } from "@/types/attack-paths";
6+
7+
import { QueryParametersForm } from "./query-parameters-form";
8+
9+
const mockQuery: AttackPathQuery = {
10+
type: "attack-paths-scans",
11+
id: "query-with-string-parameter",
12+
attributes: {
13+
name: "Query With String Parameter",
14+
short_description: "Requires a tag key",
15+
description: "Returns buckets filtered by tag",
16+
provider: "aws",
17+
attribution: null,
18+
parameters: [
19+
{
20+
name: "tag_key",
21+
label: "Tag key",
22+
data_type: "string",
23+
description: "Tag key to filter the S3 bucket.",
24+
placeholder: "DataClassification",
25+
required: true,
26+
},
27+
],
28+
},
29+
};
30+
31+
function TestForm() {
32+
const form = useForm({
33+
defaultValues: {
34+
tag_key: "",
35+
},
36+
});
37+
38+
return (
39+
<FormProvider {...form}>
40+
<QueryParametersForm selectedQuery={mockQuery} />
41+
</FormProvider>
42+
);
43+
}
44+
45+
describe("QueryParametersForm", () => {
46+
it("uses the field description as the placeholder instead of rendering helper text below", () => {
47+
// Given
48+
render(<TestForm />);
49+
50+
// When
51+
const input = screen.getByRole("textbox", { name: /tag key/i });
52+
53+
// Then
54+
expect(input).toHaveAttribute("data-slot", "input");
55+
expect(input).toHaveAttribute(
56+
"placeholder",
57+
"Tag key to filter the S3 bucket.",
58+
);
59+
expect(screen.getByTestId("query-parameters-grid")).toHaveClass(
60+
"grid",
61+
"grid-cols-1",
62+
"md:grid-cols-2",
63+
);
64+
expect(screen.getByText("Tag key")).toHaveClass(
65+
"text-text-neutral-tertiary",
66+
"text-xs",
67+
"font-medium",
68+
);
69+
expect(
70+
screen.queryByText("Tag key to filter the S3 bucket."),
71+
).not.toBeInTheDocument();
72+
});
73+
});

ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const QuerySelector = ({
2727
return (
2828
<Select value={selectedQueryId || ""} onValueChange={onQueryChange}>
2929
<SelectTrigger className="w-full text-left">
30-
<SelectValue placeholder="Choose a query..." />
30+
<SelectValue placeholder="Choose a query" />
3131
</SelectTrigger>
3232
<SelectContent>
3333
{queries.map((query) => (
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { flexRender } from "@tanstack/react-table";
2+
import { render, screen } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import type { ReactNode } from "react";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
import type { AttackPathScan } from "@/types/attack-paths";
8+
9+
import { ScanListTable } from "./scan-list-table";
10+
11+
const { pushMock, navigationState } = vi.hoisted(() => ({
12+
pushMock: vi.fn(),
13+
navigationState: {
14+
pathname: "/attack-paths",
15+
searchParams: new URLSearchParams("scanPage=1&scanPageSize=5"),
16+
},
17+
}));
18+
19+
vi.mock("next/navigation", () => ({
20+
usePathname: () => navigationState.pathname,
21+
useRouter: () => ({
22+
push: pushMock,
23+
}),
24+
useSearchParams: () => navigationState.searchParams,
25+
}));
26+
27+
vi.mock("@/components/ui/entities/entity-info", () => ({
28+
EntityInfo: ({
29+
entityAlias,
30+
entityId,
31+
}: {
32+
entityAlias?: string;
33+
entityId?: string;
34+
}) => <div>{entityAlias ?? entityId}</div>,
35+
}));
36+
37+
vi.mock("@/components/ui/entities/date-with-time", () => ({
38+
DateWithTime: ({ dateTime }: { dateTime: string }) => <span>{dateTime}</span>,
39+
}));
40+
41+
vi.mock("@/components/ui/table", () => ({
42+
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
43+
DataTable: ({
44+
columns,
45+
data,
46+
metadata,
47+
controlledPage,
48+
}: {
49+
columns: Array<{
50+
id?: string;
51+
header?:
52+
| string
53+
| ((context: { column: { getCanSort: () => boolean } }) => ReactNode);
54+
cell?: (context: { row: { original: AttackPathScan } }) => ReactNode;
55+
}>;
56+
data: AttackPathScan[];
57+
metadata: {
58+
pagination: {
59+
count: number;
60+
pages: number;
61+
};
62+
};
63+
controlledPage: number;
64+
}) => (
65+
<div>
66+
<span>{metadata.pagination.count} Total Entries</span>
67+
<span>
68+
Page {controlledPage} of {metadata.pagination.pages}
69+
</span>
70+
<table>
71+
<thead>
72+
<tr>
73+
{columns.map((column, index) => (
74+
<th key={column.id ?? index}>
75+
{typeof column.header === "function"
76+
? flexRender(column.header, {
77+
column: { getCanSort: () => false },
78+
})
79+
: column.header}
80+
</th>
81+
))}
82+
</tr>
83+
</thead>
84+
<tbody>
85+
{data.map((row) => (
86+
<tr key={row.id}>
87+
{columns.map((column, index) => (
88+
<td key={column.id ?? index}>
89+
{column.cell
90+
? flexRender(column.cell, { row: { original: row } })
91+
: null}
92+
</td>
93+
))}
94+
</tr>
95+
))}
96+
</tbody>
97+
</table>
98+
</div>
99+
),
100+
}));
101+
102+
const createScan = (id: number): AttackPathScan => ({
103+
type: "attack-paths-scans",
104+
id: `scan-${id}`,
105+
attributes: {
106+
state: "completed",
107+
progress: 100,
108+
graph_data_ready: true,
109+
provider_alias: `Provider ${id}`,
110+
provider_type: "aws",
111+
provider_uid: `1234567890${id}`,
112+
inserted_at: "2026-03-11T10:00:00Z",
113+
started_at: "2026-03-11T10:00:00Z",
114+
completed_at: "2026-03-11T10:05:00Z",
115+
duration: 300,
116+
},
117+
relationships: {
118+
provider: {
119+
data: {
120+
type: "providers",
121+
id: `provider-${id}`,
122+
},
123+
},
124+
scan: {
125+
data: {
126+
type: "scans",
127+
id: `base-scan-${id}`,
128+
},
129+
},
130+
task: {
131+
data: {
132+
type: "tasks",
133+
id: `task-${id}`,
134+
},
135+
},
136+
},
137+
});
138+
139+
describe("ScanListTable", () => {
140+
beforeEach(() => {
141+
pushMock.mockReset();
142+
navigationState.searchParams = new URLSearchParams(
143+
"scanPage=1&scanPageSize=5",
144+
);
145+
});
146+
147+
it("uses the shared data table chrome and preserves query params when selecting a scan", async () => {
148+
const user = userEvent.setup();
149+
150+
render(
151+
<ScanListTable
152+
scans={Array.from({ length: 12 }, (_, index) => createScan(index + 1))}
153+
/>,
154+
);
155+
156+
expect(screen.getByText("12 Total Entries")).toBeInTheDocument();
157+
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
158+
159+
await user.click(screen.getAllByRole("button", { name: "Select scan" })[0]);
160+
161+
expect(pushMock).toHaveBeenCalledWith(
162+
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
163+
);
164+
});
165+
});

0 commit comments

Comments
 (0)