mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-15 00:57:55 +00:00
Compare commits
6 Commits
dependabot
...
fix/ui-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0669e608b4 | ||
|
|
7b83be9d4f | ||
|
|
a120c91f1d | ||
|
|
bc94b46cab | ||
|
|
b7b6077f6f | ||
|
|
257495ef79 |
@@ -2,11 +2,12 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.23.1] (Prowler UNRELEASED)
|
||||
## [1.24.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- 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)
|
||||
|
||||
---
|
||||
|
||||
@@ -35,10 +36,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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
135
ui/actions/providers/providers.test.ts
Normal file
135
ui/actions/providers/providers.test.ts
Normal file
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-4 md:justify-end">
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProvidersTableFallback = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -120,17 +106,12 @@ const ProvidersAccountsContent = async ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ProvidersFilters
|
||||
filters={providersView.filters}
|
||||
providers={providersView.providers}
|
||||
actions={<ProvidersActions />}
|
||||
/>
|
||||
<ProvidersAccountsTable
|
||||
isCloud={process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"}
|
||||
metadata={providersView.metadata}
|
||||
rows={providersView.rows}
|
||||
/>
|
||||
</div>
|
||||
<ProvidersAccountsView
|
||||
isCloud={process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"}
|
||||
filters={providersView.filters}
|
||||
providers={providersView.providers}
|
||||
metadata={providersView.metadata}
|
||||
rows={providersView.rows}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Add Provider</Button>
|
||||
<ProviderWizardModal open={open} onOpenChange={setOpen} />
|
||||
<Button onClick={handleOpen}>Add Provider</Button>
|
||||
{!onOpenWizard && (
|
||||
<ProviderWizardModal open={open} onOpenChange={setOpen} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<RowSelectionState>({});
|
||||
|
||||
@@ -65,6 +73,8 @@ export function ProvidersAccountsTable({
|
||||
rowSelection,
|
||||
testableProviderIds,
|
||||
clearSelection,
|
||||
onOpenProviderWizard,
|
||||
onOpenOrganizationWizard,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
88
ui/components/providers/providers-accounts-view.tsx
Normal file
88
ui/components/providers/providers-accounts-view.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<ProvidersFilters
|
||||
filters={filters}
|
||||
providers={providers}
|
||||
actions={
|
||||
<>
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton onOpenWizard={() => openProviderWizard()} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ProvidersAccountsTable
|
||||
isCloud={isCloud}
|
||||
metadata={metadata}
|
||||
rows={rows}
|
||||
onOpenProviderWizard={openProviderWizard}
|
||||
onOpenOrganizationWizard={openOrganizationWizard}
|
||||
/>
|
||||
<ProviderWizardModal
|
||||
open={isProviderWizardOpen}
|
||||
onOpenChange={handleWizardOpenChange}
|
||||
initialData={providerWizardInitialData}
|
||||
orgInitialData={orgWizardInitialData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<ProvidersTableRow>[] {
|
||||
return [
|
||||
{
|
||||
@@ -320,6 +326,8 @@ export function getColumnProviders(
|
||||
isRowSelected={row.getIsSelected()}
|
||||
testableProviderIds={testableProviderIds}
|
||||
onClearSelection={onClearSelection}
|
||||
onOpenProviderWizard={onOpenProviderWizard}
|
||||
onOpenOrganizationWizard={onOpenOrganizationWizard}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<any>;
|
||||
}) as unknown as Row<ProvidersTableRow>;
|
||||
|
||||
const createOrgRow = () =>
|
||||
({
|
||||
@@ -117,7 +115,7 @@ const createOrgRow = () =>
|
||||
},
|
||||
],
|
||||
},
|
||||
}) as Row<any>;
|
||||
}) as unknown as Row<ProvidersTableRow>;
|
||||
|
||||
const createOuRow = () =>
|
||||
({
|
||||
@@ -142,19 +140,21 @@ const createOuRow = () =>
|
||||
},
|
||||
],
|
||||
},
|
||||
}) as Row<any>;
|
||||
}) as unknown as Row<ProvidersTableRow>;
|
||||
|
||||
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(
|
||||
<DataTableRowActions
|
||||
row={createRow()}
|
||||
row={createRow(false)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
<DataTableRowActions
|
||||
row={createRow(true)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<DataTableRowActions
|
||||
row={createRow(true)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={onOpenProviderWizard}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<DataTableRowActions
|
||||
row={createOrgRow()}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={onOpenOrganizationWizard}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void>;
|
||||
onTestChildConnections: () => Promise<void>;
|
||||
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<OrgWizardInitialData | null>(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 && (
|
||||
<>
|
||||
<Modal
|
||||
open={isEditNameOpen}
|
||||
onOpenChange={setIsEditNameOpen}
|
||||
title="Edit Organization Name"
|
||||
>
|
||||
<EditNameForm
|
||||
currentValue={rowData.name}
|
||||
label="Name"
|
||||
successMessage="The organization name was updated successfully."
|
||||
helperText="If left blank, Prowler will use the name stored in AWS."
|
||||
setIsOpen={setIsEditNameOpen}
|
||||
onSave={(name) => updateOrganizationName(rowData.id, name)}
|
||||
/>
|
||||
</Modal>
|
||||
<ProviderWizardModal
|
||||
open={isOrgWizardOpen}
|
||||
onOpenChange={setIsOrgWizardOpen}
|
||||
orgInitialData={orgWizardData ?? undefined}
|
||||
<Modal
|
||||
open={isEditNameOpen}
|
||||
onOpenChange={setIsEditNameOpen}
|
||||
title="Edit Organization Name"
|
||||
>
|
||||
<EditNameForm
|
||||
currentValue={rowData.name}
|
||||
label="Name"
|
||||
successMessage="The organization name was updated successfully."
|
||||
helperText="If left blank, Prowler will use the name stored in AWS."
|
||||
setIsOpen={setIsEditNameOpen}
|
||||
onSave={(name) => updateOrganizationName(rowData.id, name)}
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
)}
|
||||
<Modal
|
||||
open={isDeleteOrgOpen}
|
||||
@@ -205,10 +198,11 @@ export function DataTableRowActions({
|
||||
isRowSelected,
|
||||
testableProviderIds,
|
||||
onClearSelection,
|
||||
onOpenProviderWizard,
|
||||
onOpenOrganizationWizard,
|
||||
}: DataTableRowActionsProps) {
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -323,6 +317,7 @@ export function DataTableRowActions({
|
||||
onClearSelection={onClearSelection}
|
||||
onBulkTest={handleBulkTest}
|
||||
onTestChildConnections={handleTestChildConnections}
|
||||
onOpenOrganizationWizard={onOpenOrganizationWizard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -369,21 +364,6 @@ export function DataTableRowActions({
|
||||
<DeleteForm providerId={providerId} setIsOpen={setIsDeleteOpen} />
|
||||
)}
|
||||
</Modal>
|
||||
<ProviderWizardModal
|
||||
open={isWizardOpen}
|
||||
onOpenChange={setIsWizardOpen}
|
||||
initialData={{
|
||||
providerId,
|
||||
providerType,
|
||||
providerUid,
|
||||
providerAlias,
|
||||
secretId: providerSecretId,
|
||||
mode: providerSecretId
|
||||
? PROVIDER_WIZARD_MODE.UPDATE
|
||||
: PROVIDER_WIZARD_MODE.ADD,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<ActionDropdown>
|
||||
<ActionDropdownItem
|
||||
@@ -393,8 +373,19 @@ export function DataTableRowActions({
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<KeyRound />}
|
||||
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,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<Rocket />}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user