mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
fix(ui): gate provider alias edit behind Cloud subscription
- Disable "Edit Provider Alias" with a "Requires subscription" badge for Prowler Cloud accounts without an active subscription - Reuse the schedule capability as the billing-tier signal via a new canEditProviderAlias helper - Keep the action enabled in OSS/self-hosted and for subscribed accounts
This commit is contained in:
@@ -0,0 +1 @@
|
||||
`Edit Provider Alias` is now disabled with a subscription badge for Prowler Cloud accounts without an active subscription, instead of failing on submit
|
||||
@@ -468,6 +468,92 @@ describe("DataTableRowActions", () => {
|
||||
expect(screen.queryByText("Edit Scan Schedule")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables Edit Provider Alias with a subscription badge for manual-only Cloud provider rows", async () => {
|
||||
// Given a Prowler Cloud account without a subscription (trial/onboarding):
|
||||
// editing the alias would be rejected by the API, so the UI must not offer it.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<DataTableRowActions
|
||||
row={createRow(true)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
capability={SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
await user.click(screen.getByRole("button"));
|
||||
|
||||
// Then the action is still discoverable but disabled with an upsell badge.
|
||||
expect(screen.getByText("Requires subscription")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: /edit provider alias/i }),
|
||||
).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("disables Edit Provider Alias for blocked Cloud provider rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<DataTableRowActions
|
||||
row={createRow(true)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
capability={SCAN_SCHEDULE_CAPABILITY.BLOCKED}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
await user.click(screen.getByRole("button"));
|
||||
|
||||
// Then
|
||||
expect(screen.getByText("Requires subscription")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: /edit provider alias/i }),
|
||||
).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("opens Edit Provider Alias for subscribed Cloud provider rows", async () => {
|
||||
// Given a subscribed Cloud account (ADVANCED): the alias edit works.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<DataTableRowActions
|
||||
row={createRow(true)}
|
||||
hasSelection={false}
|
||||
isRowSelected={false}
|
||||
testableProviderIds={[]}
|
||||
onClearSelection={vi.fn()}
|
||||
onOpenProviderWizard={vi.fn()}
|
||||
onOpenOrganizationWizard={vi.fn()}
|
||||
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
await user.click(screen.getByRole("button"));
|
||||
|
||||
// Then the item is enabled (no upsell badge) and opens the alias modal.
|
||||
expect(screen.queryByText("Requires subscription")).not.toBeInTheDocument();
|
||||
await user.click(screen.getByText("Edit Provider Alias"));
|
||||
expect(
|
||||
screen.getByRole("dialog", { name: /edit provider alias/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens scan config management with the precomputed current config id", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type EditScanScheduleState,
|
||||
} from "@/components/scans/schedule/edit-scan-schedule-modal";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
ActionDropdown,
|
||||
ActionDropdownDangerZone,
|
||||
@@ -35,7 +36,10 @@ import {
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { runWithConcurrencyLimit } from "@/lib/concurrency";
|
||||
import { testProviderConnection } from "@/lib/provider-helpers";
|
||||
import { getScanScheduleCapability } from "@/lib/schedules";
|
||||
import {
|
||||
canEditProviderAlias,
|
||||
getScanScheduleCapability,
|
||||
} from "@/lib/schedules";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
@@ -291,9 +295,11 @@ export function DataTableRowActions({
|
||||
currentScanConfigId = null,
|
||||
capability,
|
||||
}: DataTableRowActionsProps) {
|
||||
const resolvedCapability = capability ?? getScanScheduleCapability(isCloud());
|
||||
const canEditSchedule =
|
||||
(capability ?? getScanScheduleCapability(isCloud())) ===
|
||||
SCAN_SCHEDULE_CAPABILITY.ADVANCED;
|
||||
resolvedCapability === SCAN_SCHEDULE_CAPABILITY.ADVANCED;
|
||||
// Editing a provider alias requires a Prowler Cloud subscription
|
||||
const canEditAlias = canEditProviderAlias(resolvedCapability);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
const [isScheduleOpen, setIsScheduleOpen] = useState(false);
|
||||
const [scheduleState, setScheduleState] = useState<EditScanScheduleState>({
|
||||
@@ -590,11 +596,26 @@ export function DataTableRowActions({
|
||||
)}
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<ActionDropdown>
|
||||
<ActionDropdownItem
|
||||
icon={<Pencil />}
|
||||
label="Edit Provider Alias"
|
||||
onSelect={() => setIsEditOpen(true)}
|
||||
/>
|
||||
{canEditAlias ? (
|
||||
<ActionDropdownItem
|
||||
icon={<Pencil />}
|
||||
label="Edit Provider Alias"
|
||||
onSelect={() => setIsEditOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<ActionDropdownItem
|
||||
icon={<Pencil />}
|
||||
label={
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
Edit Provider Alias
|
||||
<Badge variant="warning" size="sm">
|
||||
Requires subscription
|
||||
</Badge>
|
||||
</span>
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
<ActionDropdownItem
|
||||
icon={<Timer />}
|
||||
label="View Scan Jobs"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildScheduleAttributesFromProvider,
|
||||
buildSchedulesByProviderId,
|
||||
buildScheduleUpdatePayload,
|
||||
canEditProviderAlias,
|
||||
formatDayOfMonth,
|
||||
formatScheduleHour,
|
||||
getBrowserTimezone,
|
||||
@@ -440,6 +441,28 @@ describe("scan schedule capability", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canEditProviderAlias", () => {
|
||||
it("allows subscribed Cloud accounts (ADVANCED)", () => {
|
||||
expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.ADVANCED)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows OSS / self-hosted (DAILY_LEGACY, no billing)", () => {
|
||||
expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks Cloud trial/onboarding without a subscription (MANUAL_ONLY)", () => {
|
||||
expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks over-limit Cloud accounts (BLOCKED)", () => {
|
||||
expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.BLOCKED)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSchedulesByProviderId", () => {
|
||||
const buildSchedule = (
|
||||
id: string,
|
||||
|
||||
@@ -65,6 +65,19 @@ export function getScanScheduleCapability(
|
||||
: SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current account may edit a provider alias. Alias edits require a
|
||||
* Prowler Cloud subscription
|
||||
*/
|
||||
export function canEditProviderAlias(
|
||||
capability: ScanScheduleCapability,
|
||||
): boolean {
|
||||
return (
|
||||
capability === SCAN_SCHEDULE_CAPABILITY.ADVANCED ||
|
||||
capability === SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY
|
||||
);
|
||||
}
|
||||
|
||||
export function formatScheduleHour(hour: number): string {
|
||||
const normalizedHour = ((hour % 24) + 24) % 24;
|
||||
const period = normalizedHour >= 12 ? "pm" : "am";
|
||||
|
||||
Reference in New Issue
Block a user