mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-16 09:37:53 +00:00
Compare commits
2 Commits
chore/upda
...
fix/PROWLE
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd66c95cea | ||
|
|
645424242a |
@@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
- Findings group resource filters now strip unsupported scan parameters, display scan name instead of provider alias in filter badges, migrate mute modal from HeroUI to shadcn, and add searchable accounts/provider type selectors [(#10662)](https://github.com/prowler-cloud/prowler/pull/10662)
|
||||
- Compliance detail page header now reflects the actual provider, alias and UID of the selected scan instead of always defaulting to AWS [(#10674)](https://github.com/prowler-cloud/prowler/pull/10674)
|
||||
- Attack Path scan selector now labels buttons based on `graph_data_ready` instead of scan state, shows tooltip on disabled buttons, and displays green dot on all scan states when graph data is available [(#10694)](https://github.com/prowler-cloud/prowler/pull/10694)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -24,6 +24,36 @@ vi.mock("next/navigation", () => ({
|
||||
useSearchParams: () => navigationState.searchParams,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({
|
||||
children,
|
||||
asChild: _asChild,
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode;
|
||||
asChild?: boolean;
|
||||
}) => <div {...props}>{children}</div>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => (
|
||||
<div role="tooltip">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./scan-status-badge", () => ({
|
||||
ScanStatusBadge: ({
|
||||
status,
|
||||
graphDataReady,
|
||||
}: {
|
||||
status: string;
|
||||
graphDataReady?: boolean;
|
||||
}) => (
|
||||
<span>
|
||||
{status}
|
||||
{graphDataReady && " (graph ready)"}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/entities/entity-info", () => ({
|
||||
EntityInfo: ({
|
||||
entityAlias,
|
||||
@@ -201,6 +231,114 @@ describe("ScanListTable", () => {
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Failed");
|
||||
expect(button).toHaveTextContent("Unavailable");
|
||||
});
|
||||
|
||||
// PROWLER-1383: Button label based on graph_data_ready instead of scan state
|
||||
it("shows 'Unavailable' for scheduled scan when graph data is not ready", () => {
|
||||
const scheduledScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "scheduled",
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[scheduledScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Unavailable");
|
||||
});
|
||||
|
||||
it("shows 'Unavailable' for executing scan when graph data is not ready", () => {
|
||||
const executingScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "executing",
|
||||
progress: 45,
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[executingScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Unavailable");
|
||||
});
|
||||
|
||||
// PROWLER-1383: Enable Select on scheduled/executing scans with graph data from previous cycle
|
||||
it("enables 'Select' for executing scan when graph data is ready from previous cycle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const executingScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "executing",
|
||||
progress: 30,
|
||||
graph_data_ready: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[executingScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
|
||||
await user.click(button);
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("enables 'Select' for scheduled scan when graph data is ready from previous cycle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const scheduledScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "scheduled",
|
||||
graph_data_ready: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[scheduledScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
|
||||
await user.click(button);
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
// PROWLER-1383: Tooltip on disabled button explaining why it can't be selected
|
||||
it("shows tooltip on disabled button explaining graph data is not available", () => {
|
||||
const unavailableScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "executing",
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[unavailableScan]} />);
|
||||
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
"Graph data not yet available",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show tooltip on enabled button", () => {
|
||||
render(<ScanListTable scans={[createScan(1)]} />);
|
||||
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,17 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import { EntityInfo } from "@/components/ui/entities/entity-info";
|
||||
import { DataTable, DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { formatDuration } from "@/lib/date-utils";
|
||||
import type { MetaDataProps, ProviderType } from "@/types";
|
||||
import type { AttackPathScan, ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
import type { AttackPathScan } from "@/types/attack-paths";
|
||||
|
||||
import { ScanStatusBadge } from "./scan-status-badge";
|
||||
|
||||
@@ -20,11 +24,6 @@ interface ScanListTableProps {
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 5;
|
||||
const PAGE_SIZE_OPTIONS = [2, 5, 10, 15];
|
||||
const WAITING_STATES: readonly ScanState[] = [
|
||||
SCAN_STATES.SCHEDULED,
|
||||
SCAN_STATES.AVAILABLE,
|
||||
SCAN_STATES.EXECUTING,
|
||||
];
|
||||
|
||||
const parsePageParam = (value: string | null, fallback: number) => {
|
||||
if (!value) return fallback;
|
||||
@@ -57,15 +56,7 @@ const getSelectButtonLabel = (
|
||||
return "Select";
|
||||
}
|
||||
|
||||
if (WAITING_STATES.includes(scan.attributes.state)) {
|
||||
return "Waiting...";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
return "Select";
|
||||
return "Unavailable";
|
||||
};
|
||||
|
||||
const getSelectedRowSelection = (
|
||||
@@ -171,20 +162,35 @@ const getColumns = ({
|
||||
cell: ({ row }) => {
|
||||
const isDisabled = isSelectDisabled(row.original, selectedScanId);
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
</div>
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isDisabled && selectedScanId !== row.original.id) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="w-full max-w-24" tabIndex={0}>
|
||||
{button}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Graph data not yet available</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex justify-end">{button}</div>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
@@ -28,12 +29,12 @@ const BADGE_CONFIG: Record<
|
||||
[SCAN_STATES.EXECUTING]: {
|
||||
className: "bg-bg-warning-secondary text-text-neutral-primary",
|
||||
label: "In Progress",
|
||||
showGraphDot: false,
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.COMPLETED]: {
|
||||
className: "bg-bg-pass-secondary text-text-success-primary",
|
||||
label: "Completed",
|
||||
showGraphDot: false,
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.FAILED]: {
|
||||
className: "bg-bg-fail-secondary text-text-error-primary",
|
||||
@@ -65,14 +66,16 @@ export const ScanStatusBadge = ({
|
||||
? "Graph not available"
|
||||
: "Graph not available yet";
|
||||
|
||||
const spinner = status === SCAN_STATES.EXECUTING && (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
);
|
||||
|
||||
const icon =
|
||||
status === SCAN_STATES.EXECUTING ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className={
|
||||
graphDataReady ? "animate-spin text-green-500" : "animate-spin"
|
||||
}
|
||||
/>
|
||||
<>
|
||||
{graphDot}
|
||||
{spinner}
|
||||
</>
|
||||
) : (
|
||||
graphDot
|
||||
);
|
||||
@@ -85,7 +88,7 @@ export const ScanStatusBadge = ({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className={`${config.className} gap-2`}>
|
||||
<Badge className={cn(config.className, "gap-2")}>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
|
||||
Reference in New Issue
Block a user