fix(ui): open scan findings using the scan's UTC day (#12411)

This commit is contained in:
Pablo Fernandez Guerra (PFE)
2026-08-11 12:54:47 +02:00
committed by GitHub
parent 48ba1692e1
commit 5cfc22040a
5 changed files with 160 additions and 22 deletions
@@ -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 />}
+114
View File
@@ -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: {
+12 -10
View File
@@ -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;