mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
fix(ui): stabilize provider wizard modal and DataTable rendering (#10194)
This commit is contained in:
15
ui/app/(prowler)/providers/page.test.ts
Normal file
15
ui/app/(prowler)/providers/page.test.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("providers page", () => {
|
||||||
|
it("does not use unstable Date.now keys for the providers DataTable", () => {
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const pagePath = path.join(currentDir, "page.tsx");
|
||||||
|
const source = readFileSync(pagePath, "utf8");
|
||||||
|
|
||||||
|
expect(source).not.toContain("key={`providers-${Date.now()}`}");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -104,7 +104,6 @@ const ProvidersTable = async ({
|
|||||||
<div className="grid grid-cols-12 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<div className="col-span-12">
|
<div className="col-span-12">
|
||||||
<DataTable
|
<DataTable
|
||||||
key={`providers-${Date.now()}`}
|
|
||||||
columns={ColumnProviders}
|
columns={ColumnProviders}
|
||||||
data={enrichedProviders || []}
|
data={enrichedProviders || []}
|
||||||
metadata={providersData?.meta}
|
metadata={providersData?.meta}
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ import {
|
|||||||
|
|
||||||
import { useProviderWizardController } from "./use-provider-wizard-controller";
|
import { useProviderWizardController } from "./use-provider-wizard-controller";
|
||||||
|
|
||||||
const { pushMock } = vi.hoisted(() => ({
|
|
||||||
pushMock: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("next/navigation", () => ({
|
|
||||||
useRouter: () => ({
|
|
||||||
push: pushMock,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("next-auth/react", () => ({
|
vi.mock("next-auth/react", () => ({
|
||||||
useSession: () => ({
|
useSession: () => ({
|
||||||
data: null,
|
data: null,
|
||||||
@@ -30,9 +20,9 @@ vi.mock("next-auth/react", () => ({
|
|||||||
|
|
||||||
describe("useProviderWizardController", () => {
|
describe("useProviderWizardController", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
pushMock.mockReset();
|
|
||||||
useProviderWizardStore.getState().reset();
|
useProviderWizardStore.getState().reset();
|
||||||
useOrgSetupStore.getState().reset();
|
useOrgSetupStore.getState().reset();
|
||||||
});
|
});
|
||||||
@@ -131,7 +121,7 @@ describe("useProviderWizardController", () => {
|
|||||||
expect(onOpenChange).not.toHaveBeenCalled();
|
expect(onOpenChange).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("closes and navigates when launch footer action is triggered", () => {
|
it("does not override launch footer config in the controller", () => {
|
||||||
// Given
|
// Given
|
||||||
const onOpenChange = vi.fn();
|
const onOpenChange = vi.fn();
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@@ -146,15 +136,10 @@ describe("useProviderWizardController", () => {
|
|||||||
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resolvedFooterConfig } = result.current;
|
|
||||||
act(() => {
|
|
||||||
resolvedFooterConfig.onAction?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(pushMock).toHaveBeenCalledWith("/scans");
|
expect(result.current.resolvedFooterConfig.showAction).toBe(false);
|
||||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
expect(result.current.resolvedFooterConfig.showBack).toBe(false);
|
||||||
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
expect(onOpenChange).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not reset organizations step when org store updates while modal is open", () => {
|
it("does not reset organizations step when org store updates while modal is open", () => {
|
||||||
@@ -188,4 +173,69 @@ describe("useProviderWizardController", () => {
|
|||||||
expect(result.current.wizardVariant).toBe("organizations");
|
expect(result.current.wizardVariant).toBe("organizations");
|
||||||
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.VALIDATE);
|
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.VALIDATE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not rehydrate wizard state when initial data changes while modal remains open", async () => {
|
||||||
|
// Given
|
||||||
|
const onOpenChange = vi.fn();
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({
|
||||||
|
open,
|
||||||
|
initialData,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
initialData?: {
|
||||||
|
providerId: string;
|
||||||
|
providerType: "gcp";
|
||||||
|
providerUid: string;
|
||||||
|
providerAlias: string;
|
||||||
|
secretId: string | null;
|
||||||
|
mode: "add" | "update";
|
||||||
|
};
|
||||||
|
}) =>
|
||||||
|
useProviderWizardController({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
initialData,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
open: true,
|
||||||
|
initialData: {
|
||||||
|
providerId: "provider-1",
|
||||||
|
providerType: "gcp",
|
||||||
|
providerUid: "project-123",
|
||||||
|
providerAlias: "gcp-main",
|
||||||
|
secretId: null,
|
||||||
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useProviderWizardStore.getState().setVia("service-account");
|
||||||
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.TEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When: provider data refreshes while modal is still open
|
||||||
|
rerender({
|
||||||
|
open: true,
|
||||||
|
initialData: {
|
||||||
|
providerId: "provider-1",
|
||||||
|
providerType: "gcp",
|
||||||
|
providerUid: "project-123",
|
||||||
|
providerAlias: "gcp-main",
|
||||||
|
secretId: "secret-1",
|
||||||
|
mode: PROVIDER_WIZARD_MODE.UPDATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then: keep user progress in the current flow
|
||||||
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.TEST);
|
||||||
|
expect(useProviderWizardStore.getState().via).toBe("service-account");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
|
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
|
||||||
import { useOrgSetupStore } from "@/store/organizations/store";
|
import { useOrgSetupStore } from "@/store/organizations/store";
|
||||||
@@ -63,7 +62,7 @@ export function useProviderWizardController({
|
|||||||
const initialSecretId = initialData?.secretId ?? null;
|
const initialSecretId = initialData?.secretId ?? null;
|
||||||
const initialVia = initialData?.via ?? null;
|
const initialVia = initialData?.via ?? null;
|
||||||
const initialMode = initialData?.mode ?? null;
|
const initialMode = initialData?.mode ?? null;
|
||||||
const router = useRouter();
|
const hasHydratedForCurrentOpenRef = useRef(false);
|
||||||
const [wizardVariant, setWizardVariant] = useState<WizardVariant>(
|
const [wizardVariant, setWizardVariant] = useState<WizardVariant>(
|
||||||
WIZARD_VARIANT.PROVIDER,
|
WIZARD_VARIANT.PROVIDER,
|
||||||
);
|
);
|
||||||
@@ -95,9 +94,15 @@ export function useProviderWizardController({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
hasHydratedForCurrentOpenRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasHydratedForCurrentOpenRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasHydratedForCurrentOpenRef.current = true;
|
||||||
|
|
||||||
if (initialProviderId && initialProviderType && initialProviderUid) {
|
if (initialProviderId && initialProviderType && initialProviderUid) {
|
||||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||||
setProvider({
|
setProvider({
|
||||||
@@ -198,25 +203,7 @@ export function useProviderWizardController({
|
|||||||
const docsLink = isProviderFlow
|
const docsLink = isProviderFlow
|
||||||
? getProviderHelpText(providerTypeHint ?? providerType ?? "").link
|
? getProviderHelpText(providerTypeHint ?? providerType ?? "").link
|
||||||
: DOCS_URLS.AWS_ORGANIZATIONS;
|
: DOCS_URLS.AWS_ORGANIZATIONS;
|
||||||
const resolvedFooterConfig: WizardFooterConfig =
|
const resolvedFooterConfig: WizardFooterConfig = footerConfig;
|
||||||
isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH
|
|
||||||
? {
|
|
||||||
showBack: true,
|
|
||||||
backLabel: "Back",
|
|
||||||
onBack: () => setCurrentStep(PROVIDER_WIZARD_STEP.TEST),
|
|
||||||
showSecondaryAction: false,
|
|
||||||
secondaryActionLabel: "",
|
|
||||||
secondaryActionVariant: "outline",
|
|
||||||
secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
|
||||||
showAction: true,
|
|
||||||
actionLabel: "Go to scans",
|
|
||||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
|
||||||
onAction: () => {
|
|
||||||
handleClose();
|
|
||||||
router.push("/scans");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: footerConfig;
|
|
||||||
const modalTitle = getProviderWizardModalTitle(mode);
|
const modalTitle = getProviderWizardModalTitle(mode);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
|
|||||||
import { PROVIDER_WIZARD_STEP } from "@/types/provider-wizard";
|
import { PROVIDER_WIZARD_STEP } from "@/types/provider-wizard";
|
||||||
|
|
||||||
import { useProviderWizardController } from "./hooks/use-provider-wizard-controller";
|
import { useProviderWizardController } from "./hooks/use-provider-wizard-controller";
|
||||||
import { getOrganizationsStepperOffset } from "./provider-wizard-modal.utils";
|
import {
|
||||||
|
getOrganizationsStepperOffset,
|
||||||
|
getProviderWizardDocsDestination,
|
||||||
|
} from "./provider-wizard-modal.utils";
|
||||||
import { ConnectStep } from "./steps/connect-step";
|
import { ConnectStep } from "./steps/connect-step";
|
||||||
import { CredentialsStep } from "./steps/credentials-step";
|
import { CredentialsStep } from "./steps/credentials-step";
|
||||||
import { WIZARD_FOOTER_ACTION_TYPE } from "./steps/footer-controls";
|
import { WIZARD_FOOTER_ACTION_TYPE } from "./steps/footer-controls";
|
||||||
@@ -62,6 +65,7 @@ export function ProviderWizardModal({
|
|||||||
enabled: open,
|
enabled: open,
|
||||||
refreshToken: scrollHintRefreshToken,
|
refreshToken: scrollHintRefreshToken,
|
||||||
});
|
});
|
||||||
|
const docsDestination = getProviderWizardDocsDestination(docsLink);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -80,7 +84,7 @@ export function ProviderWizardModal({
|
|||||||
<Button variant="link" size="link-sm" className="h-auto p-0" asChild>
|
<Button variant="link" size="link-sm" className="h-auto p-0" asChild>
|
||||||
<a href={docsLink} target="_blank" rel="noopener noreferrer">
|
<a href={docsLink} target="_blank" rel="noopener noreferrer">
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
<span>Prowler Docs</span>
|
<span>{`Prowler Docs (${docsDestination})`}</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +140,11 @@ export function ProviderWizardModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH && (
|
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH && (
|
||||||
<LaunchStep />
|
<LaunchStep
|
||||||
|
onBack={() => setCurrentStep(PROVIDER_WIZARD_STEP.TEST)}
|
||||||
|
onClose={handleClose}
|
||||||
|
onFooterChange={setFooterConfig}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.SETUP && (
|
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.SETUP && (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getOrganizationsStepperOffset,
|
getOrganizationsStepperOffset,
|
||||||
|
getProviderWizardDocsDestination,
|
||||||
getProviderWizardModalTitle,
|
getProviderWizardModalTitle,
|
||||||
} from "./provider-wizard-modal.utils";
|
} from "./provider-wizard-modal.utils";
|
||||||
|
|
||||||
@@ -50,3 +51,21 @@ describe("getProviderWizardModalTitle", () => {
|
|||||||
expect(title).toBe("Update Provider Credentials");
|
expect(title).toBe("Update Provider Credentials");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getProviderWizardDocsDestination", () => {
|
||||||
|
it("returns a compact provider label for short provider docs links", () => {
|
||||||
|
const destination = getProviderWizardDocsDestination(
|
||||||
|
"https://goto.prowler.com/provider-aws",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(destination).toBe("aws");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a compact destination label for long docs links", () => {
|
||||||
|
const destination = getProviderWizardDocsDestination(
|
||||||
|
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(destination).toBe("aws-organizations");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,3 +27,21 @@ export function getProviderWizardModalTitle(mode: ProviderWizardMode) {
|
|||||||
|
|
||||||
return "Adding A Cloud Provider";
|
return "Adding A Cloud Provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProviderWizardDocsDestination(docsLink: string) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(docsLink);
|
||||||
|
const pathSegments = parsed.pathname
|
||||||
|
.split("/")
|
||||||
|
.filter((segment) => segment.length > 0);
|
||||||
|
const lastSegment = pathSegments.at(-1);
|
||||||
|
|
||||||
|
if (!lastSegment) {
|
||||||
|
return parsed.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastSegment.replace(/^provider-/, "").replace(/^prowler-cloud-/, "");
|
||||||
|
} catch {
|
||||||
|
return docsLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
85
ui/components/providers/wizard/steps/launch-step.test.tsx
Normal file
85
ui/components/providers/wizard/steps/launch-step.test.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { act, render, waitFor } from "@testing-library/react";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||||
|
|
||||||
|
import { LaunchStep } from "./launch-step";
|
||||||
|
|
||||||
|
const { scheduleDailyMock, scanOnDemandMock, toastMock } = vi.hoisted(() => ({
|
||||||
|
scheduleDailyMock: vi.fn(),
|
||||||
|
scanOnDemandMock: vi.fn(),
|
||||||
|
toastMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/actions/scans", () => ({
|
||||||
|
scheduleDaily: scheduleDailyMock,
|
||||||
|
scanOnDemand: scanOnDemandMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui", () => ({
|
||||||
|
ToastAction: ({ children, ...props }: ComponentProps<"button">) => (
|
||||||
|
<button {...props}>{children}</button>
|
||||||
|
),
|
||||||
|
useToast: () => ({
|
||||||
|
toast: toastMock,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("LaunchStep", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
localStorage.clear();
|
||||||
|
scheduleDailyMock.mockReset();
|
||||||
|
scanOnDemandMock.mockReset();
|
||||||
|
toastMock.mockReset();
|
||||||
|
useProviderWizardStore.getState().reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("launches a daily scan and shows toast", async () => {
|
||||||
|
// Given
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const onFooterChange = vi.fn();
|
||||||
|
useProviderWizardStore.setState({
|
||||||
|
providerId: "provider-1",
|
||||||
|
providerType: "gcp",
|
||||||
|
providerUid: "project-123",
|
||||||
|
mode: "add",
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduleDailyMock.mockResolvedValue({ data: { id: "scan-1" } });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LaunchStep
|
||||||
|
onBack={vi.fn()}
|
||||||
|
onClose={onClose}
|
||||||
|
onFooterChange={onFooterChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFooterChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const initialFooterConfig = onFooterChange.mock.calls.at(-1)?.[0];
|
||||||
|
await act(async () => {
|
||||||
|
initialFooterConfig.onAction?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(scheduleDailyMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentFormData = scheduleDailyMock.mock.calls[0]?.[0] as FormData;
|
||||||
|
expect(sentFormData.get("providerId")).toBe("provider-1");
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
expect(scanOnDemandMock).not.toHaveBeenCalled();
|
||||||
|
expect(toastMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Scan Launched",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,162 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { scanOnDemand, scheduleDaily } from "@/actions/scans";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/shadcn/select/select";
|
||||||
|
import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner";
|
||||||
|
import { TreeStatusIcon } from "@/components/shadcn/tree-view/tree-status-icon";
|
||||||
|
import { ToastAction, useToast } from "@/components/ui";
|
||||||
|
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||||
|
import { TREE_ITEM_STATUS } from "@/types/tree";
|
||||||
|
|
||||||
|
import {
|
||||||
|
WIZARD_FOOTER_ACTION_TYPE,
|
||||||
|
WizardFooterConfig,
|
||||||
|
} from "./footer-controls";
|
||||||
|
|
||||||
|
const SCAN_SCHEDULE = {
|
||||||
|
DAILY: "daily",
|
||||||
|
SINGLE: "single",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type ScanScheduleOption = (typeof SCAN_SCHEDULE)[keyof typeof SCAN_SCHEDULE];
|
||||||
|
|
||||||
|
interface LaunchStepProps {
|
||||||
|
onBack: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onFooterChange: (config: WizardFooterConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LaunchStep({
|
||||||
|
onBack,
|
||||||
|
onClose,
|
||||||
|
onFooterChange,
|
||||||
|
}: LaunchStepProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { providerId } = useProviderWizardStore();
|
||||||
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
|
const [scheduleOption, setScheduleOption] = useState<ScanScheduleOption>(
|
||||||
|
SCAN_SCHEDULE.DAILY,
|
||||||
|
);
|
||||||
|
const launchActionRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
|
const handleLaunchScan = async () => {
|
||||||
|
if (!providerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLaunching(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set("providerId", providerId);
|
||||||
|
const result =
|
||||||
|
scheduleOption === SCAN_SCHEDULE.DAILY
|
||||||
|
? await scheduleDaily(formData)
|
||||||
|
: await scanOnDemand(formData);
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setIsLaunching(false);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Unable to launch scan",
|
||||||
|
description: String(result.error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLaunching(false);
|
||||||
|
onClose();
|
||||||
|
toast({
|
||||||
|
title: "Scan Launched",
|
||||||
|
description:
|
||||||
|
scheduleOption === SCAN_SCHEDULE.DAILY
|
||||||
|
? "Daily scan scheduled successfully."
|
||||||
|
: "Single scan launched successfully.",
|
||||||
|
action: (
|
||||||
|
<ToastAction altText="Go to scans" asChild>
|
||||||
|
<Link href="/scans">Go to scans</Link>
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
launchActionRef.current = () => {
|
||||||
|
void handleLaunchScan();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFooterChange({
|
||||||
|
showBack: true,
|
||||||
|
backLabel: "Back",
|
||||||
|
backDisabled: isLaunching,
|
||||||
|
onBack,
|
||||||
|
showAction: true,
|
||||||
|
actionLabel: isLaunching ? "Launching scans..." : "Launch scan",
|
||||||
|
actionDisabled: isLaunching || !providerId,
|
||||||
|
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||||
|
onAction: () => {
|
||||||
|
launchActionRef.current();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [isLaunching, onBack, onFooterChange, providerId]);
|
||||||
|
|
||||||
|
if (isLaunching) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[320px] items-center justify-center">
|
||||||
|
<div className="flex items-center gap-3 py-2">
|
||||||
|
<TreeSpinner className="size-6" />
|
||||||
|
<p className="text-sm font-medium">Launching scans...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function LaunchStep() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[320px] flex-col items-center justify-center gap-4 text-center">
|
<div className="flex min-h-0 flex-1 flex-col gap-6">
|
||||||
<CheckCircle2 className="text-success size-12" />
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-xl font-semibold">Provider connected successfully</h3>
|
<TreeStatusIcon status={TREE_ITEM_STATUS.SUCCESS} className="size-6" />
|
||||||
<p className="text-muted-foreground text-sm">
|
<h3 className="text-sm font-semibold">Connection validated!</h3>
|
||||||
Continue with the action button to go to scans.
|
</div>
|
||||||
|
|
||||||
|
<p className="text-text-neutral-secondary text-sm">
|
||||||
|
Choose how you want to launch scans for this provider.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{!providerId && (
|
||||||
|
<p className="text-text-error-primary text-sm">
|
||||||
|
Provider data is missing. Go back and test the connection again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-text-neutral-secondary text-sm">Scan schedule</p>
|
||||||
|
<Select
|
||||||
|
value={scheduleOption}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setScheduleOption(value as ScanScheduleOption)
|
||||||
|
}
|
||||||
|
disabled={isLaunching || !providerId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full max-w-[376px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={SCAN_SCHEDULE.DAILY}>
|
||||||
|
Scan Daily (every 24 hours)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={SCAN_SCHEDULE.SINGLE}>
|
||||||
|
Run a single scan (no recurring schedule)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||||
@@ -6,8 +7,9 @@ import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
|||||||
|
|
||||||
import { TestConnectionStep } from "./test-connection-step";
|
import { TestConnectionStep } from "./test-connection-step";
|
||||||
|
|
||||||
const { getProviderMock } = vi.hoisted(() => ({
|
const { getProviderMock, loadingFromFormMock } = vi.hoisted(() => ({
|
||||||
getProviderMock: vi.fn(),
|
getProviderMock: vi.fn(),
|
||||||
|
loadingFromFormMock: { current: false },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/actions/providers", () => ({
|
vi.mock("@/actions/providers", () => ({
|
||||||
@@ -15,7 +17,19 @@ vi.mock("@/actions/providers", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../workflow/forms/test-connection-form", () => ({
|
vi.mock("../../workflow/forms/test-connection-form", () => ({
|
||||||
TestConnectionForm: () => <div data-testid="test-connection-form" />,
|
TestConnectionForm: ({
|
||||||
|
onLoadingChange,
|
||||||
|
}: {
|
||||||
|
onLoadingChange?: (isLoading: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadingFromFormMock.current) {
|
||||||
|
onLoadingChange?.(true);
|
||||||
|
}
|
||||||
|
}, [onLoadingChange]);
|
||||||
|
|
||||||
|
return <div data-testid="test-connection-form" />;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("TestConnectionStep", () => {
|
describe("TestConnectionStep", () => {
|
||||||
@@ -23,6 +37,7 @@ describe("TestConnectionStep", () => {
|
|||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
getProviderMock.mockReset();
|
getProviderMock.mockReset();
|
||||||
|
loadingFromFormMock.current = false;
|
||||||
useProviderWizardStore.getState().reset();
|
useProviderWizardStore.getState().reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,4 +79,54 @@ describe("TestConnectionStep", () => {
|
|||||||
});
|
});
|
||||||
expect(useProviderWizardStore.getState().secretId).toBe("secret-1");
|
expect(useProviderWizardStore.getState().secretId).toBe("secret-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates footer action label to checking while connection test is in progress", async () => {
|
||||||
|
// Given
|
||||||
|
loadingFromFormMock.current = true;
|
||||||
|
useProviderWizardStore.setState({
|
||||||
|
providerId: "provider-1",
|
||||||
|
providerType: "gcp",
|
||||||
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
||||||
|
});
|
||||||
|
getProviderMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: "provider-1",
|
||||||
|
attributes: {
|
||||||
|
uid: "project-123",
|
||||||
|
provider: "gcp",
|
||||||
|
alias: "Main",
|
||||||
|
connection: { connected: false, last_checked_at: null },
|
||||||
|
scanner_args: {},
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
secret: { data: { type: "provider-secrets", id: "secret-1" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onFooterChange = vi.fn();
|
||||||
|
|
||||||
|
// When
|
||||||
|
render(
|
||||||
|
<TestConnectionStep
|
||||||
|
onSuccess={vi.fn()}
|
||||||
|
onResetCredentials={vi.fn()}
|
||||||
|
onFooterChange={onFooterChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFooterChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const footerConfigs = onFooterChange.mock.calls.map((call) => call[0]);
|
||||||
|
const hasCheckingState = footerConfigs.some(
|
||||||
|
(config) =>
|
||||||
|
config.actionLabel === "Checking connection..." &&
|
||||||
|
config.actionDisabled === true,
|
||||||
|
);
|
||||||
|
expect(hasCheckingState).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,10 +92,9 @@ export function TestConnectionStep({
|
|||||||
backDisabled: isFormLoading,
|
backDisabled: isFormLoading,
|
||||||
onBack: onResetCredentials,
|
onBack: onResetCredentials,
|
||||||
showAction: canSubmit,
|
showAction: canSubmit,
|
||||||
actionLabel:
|
actionLabel: isFormLoading
|
||||||
mode === PROVIDER_WIZARD_MODE.UPDATE
|
? "Checking connection..."
|
||||||
? "Check connection"
|
: "Check connection",
|
||||||
: "Launch scan",
|
|
||||||
actionDisabled: isFormLoading,
|
actionDisabled: isFormLoading,
|
||||||
actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT,
|
actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT,
|
||||||
actionFormId: formId,
|
actionFormId: formId,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Checkbox } from "@heroui/checkbox";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
@@ -14,9 +13,8 @@ import {
|
|||||||
checkConnectionProvider,
|
checkConnectionProvider,
|
||||||
deleteCredentials,
|
deleteCredentials,
|
||||||
} from "@/actions/providers";
|
} from "@/actions/providers";
|
||||||
import { scanOnDemand, scheduleDaily } from "@/actions/scans";
|
|
||||||
import { getTask } from "@/actions/task/tasks";
|
import { getTask } from "@/actions/task/tasks";
|
||||||
import { CheckIcon, RocketIcon } from "@/components/icons";
|
import { CheckIcon } from "@/components/icons";
|
||||||
import { Button } from "@/components/shadcn";
|
import { Button } from "@/components/shadcn";
|
||||||
import { useToast } from "@/components/ui";
|
import { useToast } from "@/components/ui";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
@@ -83,7 +81,6 @@ export const TestConnectionForm = ({
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isResettingCredentials, setIsResettingCredentials] = useState(false);
|
const [isResettingCredentials, setIsResettingCredentials] = useState(false);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
||||||
|
|
||||||
const formSchema = testConnectionFormSchema;
|
const formSchema = testConnectionFormSchema;
|
||||||
|
|
||||||
@@ -91,7 +88,6 @@ export const TestConnectionForm = ({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
providerId,
|
providerId,
|
||||||
runOnce: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,44 +145,12 @@ export const TestConnectionForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connected && !isUpdated) {
|
if (connected && !isUpdated) {
|
||||||
try {
|
if (onSuccess) {
|
||||||
// Check if the runOnce checkbox is checked
|
onSuccess();
|
||||||
const runOnce = form.watch("runOnce");
|
return;
|
||||||
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (runOnce) {
|
|
||||||
data = await scanOnDemand(formData);
|
|
||||||
} else {
|
|
||||||
data = await scheduleDaily(formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
setApiErrorMessage(data.error);
|
|
||||||
form.setError("providerId", {
|
|
||||||
type: "server",
|
|
||||||
message: data.error,
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Oops! Something went wrong",
|
|
||||||
description: data.error,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (onSuccess) {
|
|
||||||
onSuccess();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsRedirecting(true);
|
|
||||||
router.push("/scans");
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
form.setError("providerId", {
|
|
||||||
type: "server",
|
|
||||||
message: "An unexpected error occurred. Please try again.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return router.push("/providers");
|
||||||
} else {
|
} else {
|
||||||
setConnectionStatus({
|
setConnectionStatus({
|
||||||
connected: false,
|
connected: false,
|
||||||
@@ -235,25 +199,6 @@ export const TestConnectionForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRedirecting) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-6 py-12">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="bg-primary/20 h-24 w-24 animate-pulse rounded-full" />
|
|
||||||
<div className="border-primary absolute inset-0 h-24 w-24 animate-spin rounded-full border-4 border-t-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-primary text-xl font-medium">
|
|
||||||
Scan initiated successfully
|
|
||||||
</p>
|
|
||||||
<p className="text-small mt-2 font-bold text-gray-500">
|
|
||||||
Redirecting to scans job details...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -262,14 +207,10 @@ export const TestConnectionForm = ({
|
|||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="mb-2 text-xl font-medium">
|
<div className="mb-2 text-xl font-medium">Check connection</div>
|
||||||
{!isUpdated
|
|
||||||
? "Check connection and launch scan"
|
|
||||||
: "Check connection"}
|
|
||||||
</div>
|
|
||||||
<p className="text-small text-default-500 py-2">
|
<p className="text-small text-default-500 py-2">
|
||||||
{!isUpdated
|
{!isUpdated
|
||||||
? "After a successful connection, a scan will automatically run every 24 hours. To run a single scan instead, select the checkbox below."
|
? "After a successful connection, continue to the launch step to configure and start your scan."
|
||||||
: "A successful connection will redirect you to the providers page."}
|
: "A successful connection will redirect you to the providers page."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,20 +250,6 @@ export const TestConnectionForm = ({
|
|||||||
providerUID={providerData.data.attributes.uid}
|
providerUID={providerData.data.attributes.uid}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isUpdated && !connectionStatus?.error && (
|
|
||||||
<Checkbox
|
|
||||||
{...form.register("runOnce")}
|
|
||||||
isSelected={!!form.watch("runOnce")}
|
|
||||||
classNames={{
|
|
||||||
label: "text-small",
|
|
||||||
wrapper: "checkbox-update",
|
|
||||||
}}
|
|
||||||
color="default"
|
|
||||||
>
|
|
||||||
Run a single scan (no recurring schedule).
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isUpdated && !connectionStatus?.error && (
|
{isUpdated && !connectionStatus?.error && (
|
||||||
<p className="text-small text-default-500 py-2">
|
<p className="text-small text-default-500 py-2">
|
||||||
Check the new credentials and test the connection.
|
Check the new credentials and test the connection.
|
||||||
@@ -372,13 +299,13 @@ export const TestConnectionForm = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
!isUpdated && <RocketIcon size={24} />
|
<CheckIcon size={24} />
|
||||||
)}
|
)}
|
||||||
{isLoading
|
{isLoading
|
||||||
? "Loading"
|
? "Checking"
|
||||||
: isUpdated
|
: isUpdated
|
||||||
? "Check connection"
|
? "Check connection"
|
||||||
: "Launch scan"}
|
: "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -756,6 +756,9 @@ export class ProvidersPage extends BasePage {
|
|||||||
});
|
});
|
||||||
if (await button.isVisible().catch(() => false)) {
|
if (await button.isVisible().catch(() => false)) {
|
||||||
await button.click();
|
await button.click();
|
||||||
|
if (actionName === "Check connection") {
|
||||||
|
await this.handleCheckConnectionCompletion();
|
||||||
|
}
|
||||||
if (actionName === "Launch scan") {
|
if (actionName === "Launch scan") {
|
||||||
await this.handleLaunchScanCompletion();
|
await this.handleLaunchScanCompletion();
|
||||||
}
|
}
|
||||||
@@ -768,9 +771,9 @@ export class ProvidersPage extends BasePage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleLaunchScanCompletion(): Promise<void> {
|
private async handleCheckConnectionCompletion(): Promise<void> {
|
||||||
const goToScansButton = this.page.getByRole("button", {
|
const launchScanButton = this.page.getByRole("button", {
|
||||||
name: "Go to scans",
|
name: "Launch scan",
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
const connectionError = this.page.locator(
|
const connectionError = this.page.locator(
|
||||||
@@ -779,9 +782,9 @@ export class ProvidersPage extends BasePage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.page.waitForURL(/\/scans/, { timeout: 20000 }),
|
launchScanButton.waitFor({ state: "visible", timeout: 30000 }),
|
||||||
goToScansButton.waitFor({ state: "visible", timeout: 20000 }),
|
this.wizardModal.waitFor({ state: "hidden", timeout: 30000 }),
|
||||||
connectionError.waitFor({ state: "visible", timeout: 20000 }),
|
connectionError.waitFor({ state: "visible", timeout: 30000 }),
|
||||||
]);
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
// Continue and inspect visible state below.
|
// Continue and inspect visible state below.
|
||||||
@@ -794,14 +797,47 @@ export class ProvidersPage extends BasePage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.page.url().includes("/scans")) {
|
if (await launchScanButton.isVisible().catch(() => false)) {
|
||||||
return;
|
await launchScanButton.click();
|
||||||
|
await this.handleLaunchScanCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleLaunchScanCompletion(): Promise<void> {
|
||||||
|
const connectionError = this.page.locator(
|
||||||
|
"div.border-border-error p.text-text-error-primary",
|
||||||
|
);
|
||||||
|
const launchErrorToast = this.page.getByRole("alert").filter({
|
||||||
|
hasText: /Unable to launch scan/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
this.wizardModal.waitFor({ state: "hidden", timeout: 30000 }),
|
||||||
|
connectionError.waitFor({ state: "visible", timeout: 30000 }),
|
||||||
|
launchErrorToast.waitFor({ state: "visible", timeout: 30000 }),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
// Continue and inspect visible state below.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await goToScansButton.isVisible().catch(() => false)) {
|
if (await connectionError.isVisible().catch(() => false)) {
|
||||||
await goToScansButton.click();
|
const errorText = await connectionError.textContent();
|
||||||
await this.page.waitForURL(/\/scans/, { timeout: 30000 });
|
throw new Error(
|
||||||
|
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await launchErrorToast.isVisible().catch(() => false)) {
|
||||||
|
const errorText = await launchErrorToast.textContent();
|
||||||
|
throw new Error(
|
||||||
|
`Launch scan failed with error: ${errorText?.trim() || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(this.wizardModal).not.toBeVisible({ timeout: 30000 });
|
||||||
|
await this.page.waitForURL(/\/providers/, { timeout: 30000 });
|
||||||
|
await expect(this.providersTable).toBeVisible({ timeout: 30000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectCredentialsType(type: AWSCredentialType): Promise<void> {
|
async selectCredentialsType(type: AWSCredentialType): Promise<void> {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export class ScansPage extends BasePage {
|
|||||||
|
|
||||||
async verifyPageLoaded(): Promise<void> {
|
async verifyPageLoaded(): Promise<void> {
|
||||||
// Verify the scans page is loaded
|
// Verify the scans page is loaded
|
||||||
|
if (!this.page.url().includes("/scans")) {
|
||||||
|
await this.goto();
|
||||||
|
}
|
||||||
|
|
||||||
await expect(this.page).toHaveTitle(/Prowler/);
|
await expect(this.page).toHaveTitle(/Prowler/);
|
||||||
await expect(this.scanTable).toBeVisible();
|
await expect(this.scanTable).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user