mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
fix(ui): open scan findings using the scan's UTC day (#12411)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
`View Findings` on the Scans page no longer opens an empty list for users outside the UTC timezone
|
||||
@@ -44,11 +44,6 @@ vi.mock("@/lib/helper", () => ({
|
||||
downloadScanZip: downloadScanZipMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/date-utils", () => ({
|
||||
toLocalDateString: (value: string | null | undefined) =>
|
||||
value ? "2026-01-01" : undefined,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/scans/edit-alias-modal", () => ({
|
||||
EditAliasModal: ({
|
||||
open,
|
||||
@@ -314,13 +309,14 @@ describe("ScanJobsRowActions", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("links completed scans to filtered findings", async () => {
|
||||
it("links completed scans to filtered findings without a browser-local date", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScanJobsRowActions
|
||||
scan={makeScan({
|
||||
state: "completed",
|
||||
started_at: "2026-01-01T09:50:00Z",
|
||||
completed_at: "2026-01-01T10:05:00Z",
|
||||
})}
|
||||
tab="completed"
|
||||
@@ -335,10 +331,34 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
// Then
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/findings?filter[scan__in]=scan-1&filter[inserted_at]=2026-01-01&filter[status__in]=FAIL",
|
||||
"/findings?filter[scan__in]=scan-1&filter[status__in]=FAIL",
|
||||
);
|
||||
});
|
||||
|
||||
it("disables View Findings when the completed scan has no completion timestamp", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScanJobsRowActions
|
||||
scan={makeScan({ state: "completed", completed_at: "" })}
|
||||
tab="completed"
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: /open actions menu/i }),
|
||||
);
|
||||
const viewFindings = screen.getByRole("menuitem", {
|
||||
name: /view findings/i,
|
||||
});
|
||||
await user.click(viewFindings);
|
||||
|
||||
// Then
|
||||
expect(viewFindings).toHaveAttribute("aria-disabled", "true");
|
||||
expect(pushMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("triggers downloadScanZip with the scan id when downloading reports", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
ActionDropdownItem,
|
||||
} from "@/components/shadcn/dropdown";
|
||||
import { buildPerScanComplianceHref } from "@/lib/compliance/compliance-tab-url";
|
||||
import { toLocalDateString } from "@/lib/date-utils";
|
||||
import { downloadScanZip } from "@/lib/helper";
|
||||
import { getScanScheduleCapability } from "@/lib/schedules";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
@@ -80,7 +79,9 @@ export function ScanJobsRowActions({
|
||||
const isCompleted = scanState === "completed";
|
||||
const isFailed = scanState === "failed";
|
||||
const taskId = scan.relationships.task.data?.id;
|
||||
const scanDate = toLocalDateString(scan.attributes.completed_at);
|
||||
// The findings page bounds the UTC day range with completed_at; without it the
|
||||
// range collapses to the start day and can miss later findings.
|
||||
const hasCompletedAt = Boolean(scan.attributes.completed_at);
|
||||
const providerId = scan.relationships.provider.data?.id;
|
||||
const scheduleProvider: ScanScheduleProvider | undefined = providerId
|
||||
? {
|
||||
@@ -92,9 +93,9 @@ export function ScanJobsRowActions({
|
||||
: undefined;
|
||||
|
||||
const openFindings = () => {
|
||||
if (!isCompleted || !scanDate) return;
|
||||
if (!isCompleted || !hasCompletedAt) return;
|
||||
router.push(
|
||||
`/findings?filter[scan__in]=${scan.id}&filter[inserted_at]=${scanDate}&filter[status__in]=FAIL`,
|
||||
`/findings?filter[scan__in]=${scan.id}&filter[status__in]=FAIL`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -202,7 +203,7 @@ export function ScanJobsRowActions({
|
||||
icon={<Eye />}
|
||||
label="View Findings"
|
||||
onSelect={openFindings}
|
||||
disabled={!isCompleted || !scanDate}
|
||||
disabled={!isCompleted || !hasCompletedAt}
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<ShieldCheck />}
|
||||
|
||||
@@ -87,6 +87,120 @@ describe("resolveFindingScanDateFilters", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a single day when the scan starts and completes on the same UTC day", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
"filter[scan__in]": "scan-1",
|
||||
},
|
||||
scans: [
|
||||
{
|
||||
id: "scan-1",
|
||||
attributes: {
|
||||
started_at: "2026-04-07T09:40:00Z",
|
||||
completed_at: "2026-04-07T10:00:00Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
loadScan: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
"filter[scan__in]": "scan-1",
|
||||
"filter[inserted_at]": "2026-04-07",
|
||||
});
|
||||
});
|
||||
|
||||
it("covers both UTC days for a scan that crosses midnight while inserting findings", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
"filter[scan__in]": "scan-1",
|
||||
},
|
||||
scans: [
|
||||
{
|
||||
id: "scan-1",
|
||||
attributes: {
|
||||
started_at: "2026-04-06T23:40:00Z",
|
||||
completed_at: "2026-04-07T00:15:00Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
loadScan: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
"filter[scan__in]": "scan-1",
|
||||
"filter[inserted_at__gte]": "2026-04-06",
|
||||
"filter[inserted_at__lte]": "2026-04-07",
|
||||
});
|
||||
});
|
||||
|
||||
it("derives the day from started_at when the scan has not completed yet", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
"filter[scan__in]": "scan-1",
|
||||
},
|
||||
scans: [
|
||||
{
|
||||
id: "scan-1",
|
||||
attributes: {
|
||||
started_at: "2026-04-07T09:40:00Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
loadScan: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
"filter[scan__in]": "scan-1",
|
||||
"filter[inserted_at]": "2026-04-07",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves the filters untouched when the scan exposes no timestamps", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
"filter[scan__in]": "scan-1",
|
||||
},
|
||||
scans: [{ id: "scan-1", attributes: {} }],
|
||||
loadScan: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
"filter[scan__in]": "scan-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("spans every selected scan when several scans are filtered at once", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
"filter[scan__in]": "scan-1,scan-2",
|
||||
},
|
||||
scans: [
|
||||
{
|
||||
id: "scan-1",
|
||||
attributes: {
|
||||
started_at: "2026-04-05T23:50:00Z",
|
||||
completed_at: "2026-04-06T00:10:00Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "scan-2",
|
||||
attributes: {
|
||||
started_at: "2026-04-07T08:00:00Z",
|
||||
completed_at: "2026-04-07T08:30:00Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
loadScan: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
"filter[scan__in]": "scan-1,scan-2",
|
||||
"filter[inserted_at__gte]": "2026-04-05",
|
||||
"filter[inserted_at__lte]": "2026-04-07",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not override an explicit inserted_at filter already chosen in the frontend", async () => {
|
||||
const result = await resolveFindingScanDateFilters({
|
||||
filters: {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
interface ScanDateSource {
|
||||
id: string;
|
||||
attributes?: {
|
||||
// Findings are persisted when the scan finishes, so their `inserted_at`
|
||||
// aligns with the scan's `completed_at` — not the scan's `inserted_at`
|
||||
// (which is when the scan row was first created and can fall on a
|
||||
// different UTC day for scans that cross midnight).
|
||||
// Findings are written throughout the run, so the range spans both
|
||||
// timestamps, not the scan's `inserted_at` (when the row was created).
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
};
|
||||
}
|
||||
@@ -38,10 +37,10 @@ function hasInsertedAtFilter(filters: Record<string, string>): boolean {
|
||||
}
|
||||
|
||||
export function buildFindingScanDateFilters(
|
||||
scanCompletedAtValues: string[],
|
||||
scanDateTimes: string[],
|
||||
): Record<string, string> {
|
||||
const dates = Array.from(
|
||||
new Set(scanCompletedAtValues.map(formatScanDate).filter(Boolean)),
|
||||
new Set(scanDateTimes.map(formatScanDate).filter(Boolean)),
|
||||
).sort() as string[];
|
||||
|
||||
if (dates.length === 0) {
|
||||
@@ -86,11 +85,14 @@ export async function resolveFindingScanDateFilters({
|
||||
});
|
||||
}
|
||||
|
||||
const scanCompletedAtValues = scanIds
|
||||
.map((scanId) => scansById.get(scanId)?.attributes?.completed_at)
|
||||
.filter((completedAt): completedAt is string => Boolean(completedAt));
|
||||
const scanDateTimes = scanIds
|
||||
.flatMap((scanId) => {
|
||||
const attributes = scansById.get(scanId)?.attributes;
|
||||
return [attributes?.started_at, attributes?.completed_at];
|
||||
})
|
||||
.filter((dateTime): dateTime is string => Boolean(dateTime));
|
||||
|
||||
const dateFilters = buildFindingScanDateFilters(scanCompletedAtValues);
|
||||
const dateFilters = buildFindingScanDateFilters(scanDateTimes);
|
||||
|
||||
if (Object.keys(dateFilters).length === 0) {
|
||||
return filters;
|
||||
|
||||
Reference in New Issue
Block a user