mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 23:04:23 +00:00
feat(ui): launch the initial scan by default after provider setup (#13104)
This commit is contained in:
1 parent
6ee0dd9730
commit
34e0af33c2
9 files changed
+102
-40
No files matched your search
@@ -29,8 +29,6 @@ import type { SearchParamsProps } from "@/types";
|
||||
|
||||
import { ProvidersTabContent } from "./providers-tab-content";
|
||||
|
||||
const INITIAL_SCAN_LABEL = "Launch an initial scan now for immediate findings";
|
||||
|
||||
interface MountOptions {
|
||||
/** Seed `?addProvider=true` so the wizard opens on mount. Default true. */
|
||||
openWizard?: boolean;
|
||||
@@ -911,14 +909,6 @@ export class ProvidersPageHarness extends BrowserHarness<OrgFixture> {
|
||||
|
||||
// --- Wizard: launch step ------------------------------------------------
|
||||
|
||||
/** Tick "launch an initial scan now" on the launch step's schedule form. */
|
||||
async enableInitialScan(): Promise<void> {
|
||||
const checkbox = await this.waitFor(() =>
|
||||
this.q(`[role="checkbox"][aria-label="${INITIAL_SCAN_LABEL}"]`),
|
||||
);
|
||||
await this.clickElement(checkbox);
|
||||
}
|
||||
|
||||
/** Save the scan schedules and launch the initial scans (footer action). */
|
||||
async saveScheduleAndLaunch(): Promise<void> {
|
||||
await this.clickPrimary(/Save and launch scan/);
|
||||
|
||||
@@ -203,7 +203,6 @@ describe("Organization onboarding wizard", () => {
|
||||
|
||||
expect(harness.applyCallCount).toBe(1);
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForLaunchComplete();
|
||||
|
||||
@@ -695,7 +694,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForProjectsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForPartialScheduleSave(1, 1);
|
||||
|
||||
@@ -725,7 +723,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForProjectsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForScheduleSaveFailure();
|
||||
|
||||
@@ -746,7 +743,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForProjectsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForLaunchComplete();
|
||||
|
||||
@@ -778,7 +774,6 @@ describe("Organization onboarding wizard", () => {
|
||||
expect(harness.usesAccountWording()).toBe(false);
|
||||
expect(harness.applyCallCount).toBe(1);
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForLaunchComplete();
|
||||
|
||||
@@ -1164,7 +1159,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForSubscriptionsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForPartialScheduleSave(1, 1);
|
||||
|
||||
@@ -1189,7 +1183,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForSubscriptionsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForScheduleSaveFailure();
|
||||
|
||||
@@ -1208,7 +1201,6 @@ describe("Organization onboarding wizard", () => {
|
||||
await harness.testConnections();
|
||||
await harness.waitForSubscriptionsConnected();
|
||||
|
||||
await harness.enableInitialScan();
|
||||
await harness.saveScheduleAndLaunch();
|
||||
await harness.waitForLaunchComplete();
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"Launch an initial scan now" is ticked by default on the provider and organization setup launch steps, so saving a schedule also runs a first scan unless the box is unticked
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { ComponentProps } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -98,7 +99,7 @@ describe("OrgLaunchScan", () => {
|
||||
});
|
||||
|
||||
describe("when capability is ADVANCED", () => {
|
||||
it("should save schedules through the bulk endpoint", async () => {
|
||||
it("should save schedules through the bulk endpoint and launch the initial scans by default", async () => {
|
||||
// Given
|
||||
const onFooterChange = vi.fn();
|
||||
|
||||
@@ -113,6 +114,14 @@ describe("OrgLaunchScan", () => {
|
||||
|
||||
// When
|
||||
await screen.findByText("Scan Schedule");
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
).toBeChecked();
|
||||
await waitFor(() =>
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe(
|
||||
"Save and launch scan",
|
||||
),
|
||||
);
|
||||
await act(async () => {
|
||||
lastFooterConfig(onFooterChange)?.onAction?.();
|
||||
});
|
||||
@@ -130,8 +139,44 @@ describe("OrgLaunchScan", () => {
|
||||
scan_timezone: "Europe/Madrid",
|
||||
}),
|
||||
);
|
||||
expect(launchOrganizationScansMock).not.toHaveBeenCalled();
|
||||
expect(launchOrganizationScansMock).toHaveBeenCalledWith("org-1");
|
||||
expect(pushMock).toHaveBeenCalledWith("/providers");
|
||||
expect(
|
||||
toastMock.mock.calls[0]?.[0].action.props.children.props.href,
|
||||
).toBe(`/scans?tab=${SCAN_JOBS_TAB.ACTIVE}`);
|
||||
});
|
||||
|
||||
it("should save only the schedules when the initial scan is unticked", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const onFooterChange = vi.fn();
|
||||
|
||||
render(
|
||||
<OrgLaunchScan
|
||||
onClose={vi.fn()}
|
||||
onBack={vi.fn()}
|
||||
onFooterChange={onFooterChange}
|
||||
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
await screen.findByText("Scan Schedule");
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe("Save"),
|
||||
);
|
||||
await act(async () => {
|
||||
lastFooterConfig(onFooterChange)?.onAction?.();
|
||||
});
|
||||
|
||||
// Then
|
||||
await waitFor(() =>
|
||||
expect(updateSchedulesBulkMock).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
expect(launchOrganizationScansMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
toastMock.mock.calls[0]?.[0].action.props.children.props.href,
|
||||
).toBe(`/scans?tab=${SCAN_JOBS_TAB.SCHEDULED}`);
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
buildScheduleUpdatePayload,
|
||||
describeSchedulesBulkFailures,
|
||||
getScanScheduleCapability,
|
||||
getScheduleFormDefaults,
|
||||
getSetupScheduleFormDefaults,
|
||||
parseSchedulesBulkResult,
|
||||
scheduleFormSchema,
|
||||
} from "@/lib/schedules";
|
||||
@@ -121,7 +121,7 @@ export function OrgLaunchScan({
|
||||
);
|
||||
const form = useForm<ScheduleFormValues>({
|
||||
resolver: zodResolver(scheduleFormSchema),
|
||||
defaultValues: getScheduleFormDefaults(),
|
||||
defaultValues: getSetupScheduleFormDefaults(),
|
||||
});
|
||||
const launchInitialScan = useWatch({
|
||||
control: form.control,
|
||||
|
||||
@@ -104,11 +104,16 @@ describe("LaunchStep", () => {
|
||||
).toBeEnabled();
|
||||
expect(screen.getByRole("combobox", { name: /repeats/i })).toBeDisabled();
|
||||
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
).toBeChecked();
|
||||
await waitFor(() => expect(onFooterChange).toHaveBeenCalled());
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe("Save");
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe(
|
||||
"Save and launch scan",
|
||||
);
|
||||
});
|
||||
|
||||
it("saves a legacy daily schedule by default", async () => {
|
||||
it("saves a legacy daily schedule and launches the initial scan by default", async () => {
|
||||
// Given
|
||||
const onClose = vi.fn();
|
||||
const onFooterChange = vi.fn();
|
||||
@@ -132,9 +137,13 @@ describe("LaunchStep", () => {
|
||||
await waitFor(() => expect(scheduleDailyMock).toHaveBeenCalledTimes(1));
|
||||
const sentFormData = scheduleDailyMock.mock.calls[0]?.[0] as FormData;
|
||||
expect(sentFormData.get("providerId")).toBe("provider-1");
|
||||
expect(scanOnDemandMock).not.toHaveBeenCalled();
|
||||
expect(scanOnDemandMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateScheduleMock).not.toHaveBeenCalled();
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
const toastPayload = toastMock.mock.calls[0]?.[0];
|
||||
expect(toastPayload.action.props.children.props.href).toBe(
|
||||
`/scans?tab=${SCAN_JOBS_TAB.ACTIVE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("launches only an on-demand scan when run now is selected", async () => {
|
||||
@@ -176,9 +185,10 @@ describe("LaunchStep", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
updateScheduleMock.mockResolvedValue({ data: { id: "provider-1" } });
|
||||
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
|
||||
});
|
||||
|
||||
it("defaults to schedule mode and saves through the new schedule API", async () => {
|
||||
it("defaults to schedule mode with the initial scan ticked, saves the schedule and launches the scan", async () => {
|
||||
// Given
|
||||
const onClose = vi.fn();
|
||||
const onFooterChange = vi.fn();
|
||||
@@ -193,14 +203,20 @@ describe("LaunchStep", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Then advanced cadence selector is enabled
|
||||
// Then advanced cadence selector is enabled and the initial scan is on
|
||||
expect(
|
||||
screen.getByRole("radio", { name: "On a schedule" }),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByRole("combobox", { name: /repeats/i }),
|
||||
).not.toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
).toBeChecked();
|
||||
await waitFor(() => expect(onFooterChange).toHaveBeenCalled());
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe(
|
||||
"Save and launch scan",
|
||||
);
|
||||
|
||||
// When
|
||||
await act(async () => {
|
||||
@@ -221,20 +237,19 @@ describe("LaunchStep", () => {
|
||||
}),
|
||||
);
|
||||
expect(scheduleDailyMock).not.toHaveBeenCalled();
|
||||
expect(scanOnDemandMock).not.toHaveBeenCalled();
|
||||
expect(scanOnDemandMock).toHaveBeenCalledTimes(1);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
const toastPayload = toastMock.mock.calls[0]?.[0];
|
||||
expect(toastPayload.action.props.children.props.href).toBe(
|
||||
`/scans?tab=${SCAN_JOBS_TAB.SCHEDULED}`,
|
||||
`/scans?tab=${SCAN_JOBS_TAB.ACTIVE}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("also launches an on-demand scan when the checkbox is checked", async () => {
|
||||
it("saves only the schedule when the initial scan is unticked", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const onFooterChange = vi.fn();
|
||||
seedConnectedProvider();
|
||||
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
|
||||
|
||||
render(
|
||||
<LaunchStep
|
||||
@@ -250,17 +265,20 @@ describe("LaunchStep", () => {
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(lastFooterConfig(onFooterChange)?.actionLabel).toBe("Save"),
|
||||
);
|
||||
await act(async () => {
|
||||
lastFooterConfig(onFooterChange)?.onAction?.();
|
||||
});
|
||||
|
||||
// Then
|
||||
await waitFor(() => expect(updateScheduleMock).toHaveBeenCalledTimes(1));
|
||||
expect(scanOnDemandMock).toHaveBeenCalledTimes(1);
|
||||
expect(scanOnDemandMock).not.toHaveBeenCalled();
|
||||
expect(scheduleDailyMock).not.toHaveBeenCalled();
|
||||
const toastPayload = toastMock.mock.calls[0]?.[0];
|
||||
expect(toastPayload.action.props.children.props.href).toBe(
|
||||
`/scans?tab=${SCAN_JOBS_TAB.ACTIVE}`,
|
||||
`/scans?tab=${SCAN_JOBS_TAB.SCHEDULED}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -304,9 +322,8 @@ describe("LaunchStep", () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not launch an initial scan when the schedule save fails", async () => {
|
||||
it("does not launch the initial scan when the schedule save fails", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const onFooterChange = vi.fn();
|
||||
seedConnectedProvider();
|
||||
updateScheduleMock.mockResolvedValue({ error: "Schedule failed" });
|
||||
@@ -322,9 +339,6 @@ describe("LaunchStep", () => {
|
||||
await waitFor(() => expect(onFooterChange).toHaveBeenCalled());
|
||||
|
||||
// When
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: /launch an initial scan now/i }),
|
||||
);
|
||||
await act(async () => {
|
||||
lastFooterConfig(onFooterChange)?.onAction?.();
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "@/lib/action-errors";
|
||||
import {
|
||||
getScanScheduleCapability,
|
||||
getScheduleFormDefaults,
|
||||
getSetupScheduleFormDefaults,
|
||||
scheduleFormSchema,
|
||||
} from "@/lib/schedules";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
@@ -99,7 +99,7 @@ export function LaunchStep({
|
||||
);
|
||||
const form = useForm<ScheduleFormValues>({
|
||||
resolver: zodResolver(scheduleFormSchema),
|
||||
defaultValues: getScheduleFormDefaults(),
|
||||
defaultValues: getSetupScheduleFormDefaults(),
|
||||
});
|
||||
|
||||
const isScheduleMode = canUseScheduleMode && mode === LAUNCH_MODE.SCHEDULE;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getScanScheduleCapability,
|
||||
getScheduleFormDefaults,
|
||||
getScheduleFormValues,
|
||||
getSetupScheduleFormDefaults,
|
||||
isScheduleConfigured,
|
||||
parseSchedulesBulkResult,
|
||||
} from "@/lib/schedules";
|
||||
@@ -219,6 +220,18 @@ describe("getScheduleFormDefaults", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSetupScheduleFormDefaults", () => {
|
||||
it("keeps the schedule defaults but ticks the initial scan", () => {
|
||||
const now = new Date(2026, 5, 10, 10, 30, 0, 0);
|
||||
|
||||
expect(getSetupScheduleFormDefaults(now)).toEqual({
|
||||
...getScheduleFormDefaults(now),
|
||||
launchInitialScan: true,
|
||||
});
|
||||
expect(getScheduleFormDefaults(now).launchInitialScan).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScheduleFormValues", () => {
|
||||
const buildAttributes = (
|
||||
overrides: Partial<ScheduleAttributes> = {},
|
||||
|
||||
@@ -107,6 +107,13 @@ export function getScheduleFormDefaults(now = new Date()): ScheduleFormValues {
|
||||
};
|
||||
}
|
||||
|
||||
/** Post-setup launch steps tick "launch an initial scan now" by default. */
|
||||
export function getSetupScheduleFormDefaults(
|
||||
now = new Date(),
|
||||
): ScheduleFormValues {
|
||||
return { ...getScheduleFormDefaults(now), launchInitialScan: true };
|
||||
}
|
||||
|
||||
export function getScheduleFormValues(
|
||||
schedule?: ScheduleAttributes | null,
|
||||
now = new Date(),
|
||||
|
||||
Reference in new issue
Block a user