diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index ded143758ec..b5060877e2c 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -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)
+- Provider wizard modal moved to a stable page-level host so the providers table refreshes after link, authenticate, and connection check without closing the modal [(#10675)](https://github.com/prowler-cloud/prowler/pull/10675)
---
@@ -40,10 +41,8 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🐞 Fixed
- Preserve query parameters in callbackUrl during invitation flow [(#10571)](https://github.com/prowler-cloud/prowler/pull/10571)
-- Deleting the active organization now switches to the target org before deleting, preventing JWT rejection from the backend [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491)
-- Clear Filters now resets all filters including muted findings and auto-applies, Clear all in pills only removes pill-visible sub-filters, and the discard icon is now an Undo text button [(#10446)](https://github.com/prowler-cloud/prowler/pull/10446)
-- Send to Jira modal now dynamically fetches and displays available issue types per project instead of hardcoding `"Task"`, fixing failures on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
-- Exclude service filter from finding group resources endpoint to prevent empty results when a service filter is active [(#10652)](https://github.com/prowler-cloud/prowler/pull/10652)
+- Attack Paths scan auto-refresh now correctly detects "available" (queued) scans as active [(#10476)](https://github.com/prowler-cloud/prowler/pull/10476)
+- Attack Paths empty state not showing when no scans exist [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469)
---
diff --git a/ui/actions/providers/providers.test.ts b/ui/actions/providers/providers.test.ts
new file mode 100644
index 00000000000..ab3037db8bc
--- /dev/null
+++ b/ui/actions/providers/providers.test.ts
@@ -0,0 +1,135 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const {
+ fetchMock,
+ getAuthHeadersMock,
+ getFormValueMock,
+ handleApiErrorMock,
+ handleApiResponseMock,
+} = vi.hoisted(() => ({
+ fetchMock: vi.fn(),
+ getAuthHeadersMock: vi.fn(),
+ getFormValueMock: vi.fn(),
+ handleApiErrorMock: vi.fn(),
+ handleApiResponseMock: vi.fn(),
+}));
+
+vi.mock("next/cache", () => ({
+ revalidatePath: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ redirect: vi.fn(),
+}));
+
+vi.mock("@/lib", () => ({
+ apiBaseUrl: "https://api.example.com/api/v1",
+ getAuthHeaders: getAuthHeadersMock,
+ getFormValue: getFormValueMock,
+ wait: vi.fn(),
+}));
+
+vi.mock("@/lib/provider-credentials/build-crendentials", () => ({
+ buildSecretConfig: vi.fn(() => ({
+ secretType: "access-secret-key",
+ secret: { key: "value" },
+ })),
+}));
+
+vi.mock("@/lib/provider-filters", () => ({
+ appendSanitizedProviderInFilters: vi.fn(),
+}));
+
+vi.mock("@/lib/server-actions-helper", () => ({
+ handleApiError: handleApiErrorMock,
+ handleApiResponse: handleApiResponseMock,
+}));
+
+import {
+ addCredentialsProvider,
+ addProvider,
+ checkConnectionProvider,
+ updateCredentialsProvider,
+} from "./providers";
+
+describe("providers actions", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", fetchMock);
+ getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
+ getFormValueMock.mockImplementation((formData: FormData, field: string) =>
+ formData.get(field),
+ );
+ handleApiErrorMock.mockReturnValue({ error: "Unexpected error" });
+ handleApiResponseMock.mockResolvedValue({ data: { id: "secret-1" } });
+ fetchMock.mockResolvedValue(
+ new Response(JSON.stringify({ data: { id: "secret-1" } }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ }),
+ );
+ });
+
+ it("should revalidate providers after linking a cloud provider", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerType", "aws");
+ formData.set("providerUid", "111111111111");
+
+ // When
+ await addProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers after adding credentials in the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+ formData.set("providerType", "aws");
+
+ // When
+ await addCredentialsProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers after updating credentials in the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+ formData.set("providerType", "oraclecloud");
+
+ // When
+ await updateCredentialsProvider("secret-1", formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+
+ it("should revalidate providers when checking connection from the wizard", async () => {
+ // Given
+ const formData = new FormData();
+ formData.set("providerId", "provider-1");
+
+ // When
+ await checkConnectionProvider(formData);
+
+ // Then
+ expect(handleApiResponseMock).toHaveBeenCalledWith(
+ expect.any(Response),
+ "/providers",
+ );
+ });
+});
diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx
index 3520670c327..8cbdfb34593 100644
--- a/ui/app/(prowler)/providers/page.tsx
+++ b/ui/app/(prowler)/providers/page.tsx
@@ -1,11 +1,6 @@
import { Suspense } from "react";
-import {
- AddProviderButton,
- MutedFindingsConfigButton,
- ProvidersAccountsTable,
- ProvidersFilters,
-} from "@/components/providers";
+import { ProvidersAccountsView } from "@/components/providers";
import { SkeletonTableProviders } from "@/components/providers/table";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
import { ContentLayout } from "@/components/ui";
@@ -56,15 +51,6 @@ export default async function Providers({
);
}
-const ProvidersActions = () => {
- return (
-
- );
-};
-
const ProvidersTableFallback = () => {
return (
@@ -120,17 +106,12 @@ const ProvidersAccountsContent = async ({
});
return (
-
+
);
};
diff --git a/ui/components/providers/add-provider-button.tsx b/ui/components/providers/add-provider-button.tsx
index 30d630c85ab..b296533cccb 100644
--- a/ui/components/providers/add-provider-button.tsx
+++ b/ui/components/providers/add-provider-button.tsx
@@ -5,13 +5,28 @@ import { useState } from "react";
import { ProviderWizardModal } from "@/components/providers/wizard";
import { Button } from "@/components/shadcn";
-export const AddProviderButton = () => {
+interface AddProviderButtonProps {
+ onOpenWizard?: () => void;
+}
+
+export const AddProviderButton = ({ onOpenWizard }: AddProviderButtonProps) => {
const [open, setOpen] = useState(false);
+ const handleOpen = () => {
+ if (onOpenWizard) {
+ onOpenWizard();
+ return;
+ }
+
+ setOpen(true);
+ };
+
return (
<>
-
-
+
+ {!onOpenWizard && (
+
+ )}
>
);
};
diff --git a/ui/components/providers/index.ts b/ui/components/providers/index.ts
index 71a18944eff..9184ceab2e7 100644
--- a/ui/components/providers/index.ts
+++ b/ui/components/providers/index.ts
@@ -5,6 +5,7 @@ export * from "./forms/delete-form";
export * from "./link-to-scans";
export * from "./muted-findings-config-button";
export * from "./providers-accounts-table";
+export * from "./providers-accounts-view";
export * from "./providers-filters";
export * from "./radio-card";
export * from "./radio-group-provider";
diff --git a/ui/components/providers/providers-accounts-table.tsx b/ui/components/providers/providers-accounts-table.tsx
index 212fd475c95..0c25b609d9b 100644
--- a/ui/components/providers/providers-accounts-table.tsx
+++ b/ui/components/providers/providers-accounts-table.tsx
@@ -3,6 +3,10 @@
import { RowSelectionState } from "@tanstack/react-table";
import { useEffect, useState } from "react";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
import { DataTable } from "@/components/ui/table";
import { MetaDataProps } from "@/types";
import {
@@ -16,6 +20,8 @@ interface ProvidersAccountsTableProps {
isCloud: boolean;
metadata?: MetaDataProps;
rows: ProvidersTableRow[];
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function computeTestableProviderIds(
@@ -48,6 +54,8 @@ export function ProvidersAccountsTable({
isCloud,
metadata,
rows,
+ onOpenProviderWizard,
+ onOpenOrganizationWizard,
}: ProvidersAccountsTableProps) {
const [rowSelection, setRowSelection] = useState
({});
@@ -65,6 +73,8 @@ export function ProvidersAccountsTable({
rowSelection,
testableProviderIds,
clearSelection,
+ onOpenProviderWizard,
+ onOpenOrganizationWizard,
);
return (
diff --git a/ui/components/providers/providers-accounts-view.tsx b/ui/components/providers/providers-accounts-view.tsx
new file mode 100644
index 00000000000..51329b55159
--- /dev/null
+++ b/ui/components/providers/providers-accounts-view.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useState } from "react";
+
+import { AddProviderButton } from "@/components/providers/add-provider-button";
+import { MutedFindingsConfigButton } from "@/components/providers/muted-findings-config-button";
+import { ProvidersAccountsTable } from "@/components/providers/providers-accounts-table";
+import { ProvidersFilters } from "@/components/providers/providers-filters";
+import { ProviderWizardModal } from "@/components/providers/wizard";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
+import type { FilterOption, MetaDataProps, ProviderProps } from "@/types";
+import type { ProvidersTableRow } from "@/types/providers-table";
+
+interface ProvidersAccountsViewProps {
+ isCloud: boolean;
+ filters: FilterOption[];
+ metadata?: MetaDataProps;
+ providers: ProviderProps[];
+ rows: ProvidersTableRow[];
+}
+
+export function ProvidersAccountsView({
+ isCloud,
+ filters,
+ metadata,
+ providers,
+ rows,
+}: ProvidersAccountsViewProps) {
+ const [isProviderWizardOpen, setIsProviderWizardOpen] = useState(false);
+ const [providerWizardInitialData, setProviderWizardInitialData] = useState<
+ ProviderWizardInitialData | undefined
+ >(undefined);
+ const [orgWizardInitialData, setOrgWizardInitialData] = useState<
+ OrgWizardInitialData | undefined
+ >(undefined);
+
+ const openProviderWizard = (initialData?: ProviderWizardInitialData) => {
+ setOrgWizardInitialData(undefined);
+ setProviderWizardInitialData(initialData);
+ setIsProviderWizardOpen(true);
+ };
+
+ const openOrganizationWizard = (initialData: OrgWizardInitialData) => {
+ setProviderWizardInitialData(undefined);
+ setOrgWizardInitialData(initialData);
+ setIsProviderWizardOpen(true);
+ };
+
+ const handleWizardOpenChange = (open: boolean) => {
+ setIsProviderWizardOpen(open);
+
+ if (!open) {
+ setProviderWizardInitialData(undefined);
+ setOrgWizardInitialData(undefined);
+ }
+ };
+
+ return (
+ <>
+
+
+ openProviderWizard()} />
+ >
+ }
+ />
+
+
+ >
+ );
+}
diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx
index 83c9a805896..1203234a317 100644
--- a/ui/components/providers/table/column-providers.tsx
+++ b/ui/components/providers/table/column-providers.tsx
@@ -9,6 +9,10 @@ import {
ShieldOff,
} from "lucide-react";
+import type {
+ OrgWizardInitialData,
+ ProviderWizardInitialData,
+} from "@/components/providers/wizard/types";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
@@ -108,6 +112,8 @@ export function getColumnProviders(
rowSelection: RowSelectionState,
testableProviderIds: string[],
onClearSelection: () => void,
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void,
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void,
): ColumnDef[] {
return [
{
@@ -320,6 +326,8 @@ export function getColumnProviders(
isRowSelected={row.getIsSelected()}
testableProviderIds={testableProviderIds}
onClearSelection={onClearSelection}
+ onOpenProviderWizard={onOpenProviderWizard}
+ onOpenOrganizationWizard={onOpenOrganizationWizard}
/>
);
},
diff --git a/ui/components/providers/table/data-table-row-actions.test.tsx b/ui/components/providers/table/data-table-row-actions.test.tsx
index 21feaf7e4e4..d72874af31b 100644
--- a/ui/components/providers/table/data-table-row-actions.test.tsx
+++ b/ui/components/providers/table/data-table-row-actions.test.tsx
@@ -3,9 +3,11 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
+import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
import {
PROVIDERS_GROUP_KIND,
PROVIDERS_ROW_TYPE,
+ ProvidersTableRow,
} from "@/types/providers-table";
const checkConnectionProviderMock = vi.hoisted(() => vi.fn());
@@ -18,10 +20,6 @@ vi.mock("@/actions/providers/providers", () => ({
checkConnectionProvider: checkConnectionProviderMock,
}));
-vi.mock("@/components/providers/wizard", () => ({
- ProviderWizardModal: () => null,
-}));
-
vi.mock("../forms/delete-form", () => ({
DeleteForm: () => null,
}));
@@ -44,7 +42,7 @@ vi.mock("@/lib/provider-helpers", () => ({
import { DataTableRowActions } from "./data-table-row-actions";
-const createRow = () =>
+const createRow = (hasSecret = false) =>
({
original: {
id: "provider-1",
@@ -74,7 +72,7 @@ const createRow = () =>
},
relationships: {
secret: {
- data: null,
+ data: hasSecret ? { id: "secret-1", type: "secrets" } : null,
},
provider_groups: {
meta: {
@@ -85,7 +83,7 @@ const createRow = () =>
},
groupNames: [],
},
- }) as Row;
+ }) as unknown as Row;
const createOrgRow = () =>
({
@@ -117,7 +115,7 @@ const createOrgRow = () =>
},
],
},
- }) as Row;
+ }) as unknown as Row;
const createOuRow = () =>
({
@@ -142,19 +140,21 @@ const createOuRow = () =>
},
],
},
- }) as Row;
+ }) as unknown as Row;
describe("DataTableRowActions", () => {
- it("renders the exact phase 1 menu actions for provider rows", async () => {
+ it("renders Add Credentials for provider rows without credentials", async () => {
// Given
const user = userEvent.setup();
render(
,
);
@@ -163,9 +163,32 @@ describe("DataTableRowActions", () => {
// Then
expect(screen.getByText("Edit Provider Alias")).toBeInTheDocument();
- expect(screen.getByText("Update Credentials")).toBeInTheDocument();
+ expect(screen.getByText("Add Credentials")).toBeInTheDocument();
expect(screen.getByText("Test Connection")).toBeInTheDocument();
expect(screen.getByText("Delete Provider")).toBeInTheDocument();
+ expect(screen.queryByText("Update Credentials")).not.toBeInTheDocument();
+ });
+
+ it("renders Update Credentials for provider rows with credentials", async () => {
+ // Given
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+
+ // Then
+ expect(screen.getByText("Update Credentials")).toBeInTheDocument();
expect(screen.queryByText("Add Credentials")).not.toBeInTheDocument();
});
@@ -178,6 +201,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -199,6 +224,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -220,6 +247,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -238,6 +267,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={["provider-child-1", "provider-standalone"]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -257,6 +288,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={["provider-ou-child-1", "provider-standalone"]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -276,6 +309,8 @@ describe("DataTableRowActions", () => {
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
+ onOpenProviderWizard={vi.fn()}
+ onOpenOrganizationWizard={vi.fn()}
/>,
);
@@ -286,4 +321,68 @@ describe("DataTableRowActions", () => {
).not.toBeInTheDocument();
expect(screen.queryByText("Update Credentials")).not.toBeInTheDocument();
});
+
+ it("opens the shared provider wizard when provider credentials action is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ const onOpenProviderWizard = vi.fn();
+
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+ await user.click(screen.getByText("Update Credentials"));
+
+ // Then
+ expect(onOpenProviderWizard).toHaveBeenCalledWith({
+ providerId: "provider-1",
+ providerType: "aws",
+ providerUid: "111111111111",
+ providerAlias: "AWS App Account",
+ secretId: "secret-1",
+ mode: "update",
+ });
+ });
+
+ it("opens the shared organization wizard when org credentials action is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ const onOpenOrganizationWizard = vi.fn();
+
+ render(
+ ,
+ );
+
+ // When
+ await user.click(screen.getByRole("button"));
+ await user.click(screen.getByText("Update Credentials"));
+
+ // Then
+ expect(onOpenOrganizationWizard).toHaveBeenCalledWith({
+ organizationId: "org-1",
+ organizationName: "My AWS Organization",
+ externalId: "o-abc123def4",
+ targetStep: ORG_WIZARD_STEP.SETUP,
+ targetPhase: ORG_SETUP_PHASE.ACCESS,
+ intent: "edit-credentials",
+ });
+ });
});
diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx
index 65d7901f8e4..9b531f272a6 100644
--- a/ui/components/providers/table/data-table-row-actions.tsx
+++ b/ui/components/providers/table/data-table-row-actions.tsx
@@ -6,10 +6,10 @@ import { useState } from "react";
import { updateOrganizationName } from "@/actions/organizations/organizations";
import { updateProvider } from "@/actions/providers";
-import { ProviderWizardModal } from "@/components/providers/wizard";
import {
ORG_WIZARD_INTENT,
OrgWizardInitialData,
+ ProviderWizardInitialData,
} from "@/components/providers/wizard/types";
import {
ActionDropdown,
@@ -44,6 +44,8 @@ interface DataTableRowActionsProps {
testableProviderIds: string[];
/** Callback to clear the row selection after bulk operation */
onClearSelection: () => void;
+ onOpenProviderWizard: (initialData?: ProviderWizardInitialData) => void;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function collectTestableChildProviderIds(rows: ProvidersTableRow[]): string[] {
@@ -69,6 +71,7 @@ interface OrgGroupDropdownActionsProps {
onClearSelection: () => void;
onBulkTest: (ids: string[]) => Promise;
onTestChildConnections: () => Promise;
+ onOpenOrganizationWizard: (initialData: OrgWizardInitialData) => void;
}
function OrgGroupDropdownActions({
@@ -80,12 +83,10 @@ function OrgGroupDropdownActions({
onClearSelection,
onBulkTest,
onTestChildConnections,
+ onOpenOrganizationWizard,
}: OrgGroupDropdownActionsProps) {
const [isDeleteOrgOpen, setIsDeleteOrgOpen] = useState(false);
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
- const [isOrgWizardOpen, setIsOrgWizardOpen] = useState(false);
- const [orgWizardData, setOrgWizardData] =
- useState(null);
const isOrgKind = rowData.groupKind === PROVIDERS_GROUP_KIND.ORGANIZATION;
const testIds = hasSelection ? testableProviderIds : childTestableIds;
@@ -97,7 +98,7 @@ function OrgGroupDropdownActions({
targetPhase: OrgWizardInitialData["targetPhase"],
intent?: OrgWizardInitialData["intent"],
) => {
- setOrgWizardData({
+ onOpenOrganizationWizard({
organizationId: rowData.id,
organizationName: rowData.name,
externalId: rowData.externalId ?? "",
@@ -105,33 +106,25 @@ function OrgGroupDropdownActions({
targetPhase,
intent,
});
- setIsOrgWizardOpen(true);
};
return (
<>
{isOrgKind && (
- <>
-
- updateOrganizationName(rowData.id, name)}
- />
-
-
+ updateOrganizationName(rowData.id, name)}
/>
- >
+
)}
);
}
@@ -369,21 +364,6 @@ export function DataTableRowActions({
)}
-
-
}
- label="Update Credentials"
- onSelect={() => setIsWizardOpen(true)}
+ label={hasSecret ? "Update Credentials" : "Add Credentials"}
+ onSelect={() =>
+ onOpenProviderWizard({
+ providerId,
+ providerType,
+ providerUid,
+ providerAlias,
+ secretId: providerSecretId,
+ mode: providerSecretId
+ ? PROVIDER_WIZARD_MODE.UPDATE
+ : PROVIDER_WIZARD_MODE.ADD,
+ })
+ }
/>
}
diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
index 9cb55dfb872..187c1e02519 100644
--- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
+++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx
@@ -9,8 +9,19 @@ import {
PROVIDER_WIZARD_STEP,
} from "@/types/provider-wizard";
+import type { ProviderWizardInitialData } from "../types";
import { useProviderWizardController } from "./use-provider-wizard-controller";
+const { refreshMock } = vi.hoisted(() => ({
+ refreshMock: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ refresh: refreshMock,
+ }),
+}));
+
vi.mock("next-auth/react", () => ({
useSession: () => ({
data: null,
@@ -21,12 +32,33 @@ vi.mock("next-auth/react", () => ({
describe("useProviderWizardController", () => {
beforeEach(() => {
vi.useRealTimers();
+ vi.clearAllMocks();
sessionStorage.clear();
localStorage.clear();
useProviderWizardStore.getState().reset();
useOrgSetupStore.getState().reset();
});
+ it("refreshes providers data when the wizard closes", () => {
+ // Given
+ const onOpenChange = vi.fn();
+ const { result } = renderHook(() =>
+ useProviderWizardController({
+ open: true,
+ onOpenChange,
+ }),
+ );
+
+ // When
+ act(() => {
+ result.current.handleClose();
+ });
+
+ // Then
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ expect(refreshMock).toHaveBeenCalledTimes(1);
+ });
+
it("hydrates update mode when initial data is provided", async () => {
// Given
const onOpenChange = vi.fn();
@@ -121,7 +153,7 @@ describe("useProviderWizardController", () => {
expect(onOpenChange).not.toHaveBeenCalled();
});
- it("closes the modal after a successful connection test in update mode", async () => {
+ it("moves to launch step after a successful connection test in update mode", async () => {
// Given
const onOpenChange = vi.fn();
const { result } = renderHook(() =>
@@ -149,8 +181,9 @@ describe("useProviderWizardController", () => {
result.current.handleTestSuccess();
});
- // Then — update mode should close the modal, not advance to launch
- expect(onOpenChange).toHaveBeenCalledWith(false);
+ // Then
+ expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.LAUNCH);
+ expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not override launch footer config in the controller", () => {
@@ -215,14 +248,7 @@ describe("useProviderWizardController", () => {
initialData,
}: {
open: boolean;
- initialData?: {
- providerId: string;
- providerType: "gcp";
- providerUid: string;
- providerAlias: string;
- secretId: string | null;
- mode: "add" | "update";
- };
+ initialData?: ProviderWizardInitialData;
}) =>
useProviderWizardController({
open,
diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
index 111427131e4..21fce019aa5 100644
--- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
+++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts
@@ -1,5 +1,6 @@
"use client";
+import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
@@ -57,6 +58,7 @@ export function useProviderWizardController({
initialData,
orgInitialData,
}: UseProviderWizardControllerProps) {
+ const router = useRouter();
const initialProviderId = initialData?.providerId ?? null;
const initialProviderType = initialData?.providerType ?? null;
const initialProviderUid = initialData?.providerUid ?? null;
@@ -183,6 +185,7 @@ export function useProviderWizardController({
setProviderTypeHint(null);
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
onOpenChange(false);
+ router.refresh();
};
const handleDialogOpenChange = (nextOpen: boolean) => {
@@ -194,10 +197,6 @@ export function useProviderWizardController({
};
const handleTestSuccess = () => {
- if (mode === PROVIDER_WIZARD_MODE.UPDATE) {
- handleClose();
- return;
- }
setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
};